本文内容均针对于18.x以下版本
setState 到底是同步还是异步?很多人可能都有这种经历,面试的时候面试官给了你一段代码,让你说出输出的内容,比如这样:
constructor(props) {
super(props);
this.state = {
val: '0'
}
}
componentDidMount() {
this.setState({
data: '1'
})
console.log("val: ", this.state.val);
// val: 0
setTimeout(() => {
this.setState({
data: '2'
})
console.log("setTimeout ", this.state.val);
//val:2
})
} 而这段代码的输出结果,第一个 console.log 会输出0 ,而第二个 console.log 会输出2 。也就是第一次 setState 的时候,它是异步的,第二次 setState 的时候,它又变成了同步的。
这是为什么呢?
只要你进入了
react的调度流程,那就是异步的。只要你没有进入react的调度流程,那就是同步的。什么东西不会进入react的调度流程?setTimeoutsetInterval,直接在DOM上绑定原生事件等。这些都不会走React的调度流程,你在这种情况下调用setState,那这次setState就是同步的。 否则就是异步的。而
setState同步执行的情况下,DOM也会被同步更新,也就意味着如果你多次setState,会导致多次更新,这是毫无意义并且浪费性能的。
react回调函数获取同步数据
我们使用this.state来访问需要的某些状态,但是需要更新或者修改state时,一般而言,我们都会使用setState()函数,从而达到更新state的目的,setState()函数执行会触发页面重新渲染UI。但是呢,setState是异步的!这就很难受。
1. 语法 :
setState(updater[, callback]) //
复制代码 updater是要改变的state对象,callback是state导致的页面重新渲染的回调,等价于componentDidUpdate 一般而言,在设置页面某些state的时候,需要先设置好state,然后再对页面的一些参数进行修改的时候,可以使用setState的回调函数。
2. 分析一下区别 不在回调中使用参数,我们在设置state后立即使用state:
this.state = {foo: 1};
this.setState({foo: 123});
console.log(this.state.foo);
// 1
//欢迎加入全栈开发交流圈一起学习交流:864305860
复制代码 在回调中调用设置好的state
this.state = {foo: 2};
this.setState({foo: 123}, ()=> {
console.log(foo);
// 123
});//欢迎加入全栈开发交流圈一起学习交流:864305860
复制代码 关于setState的回调函数的作用大概如此,这个函数相当于componentDidUpdate函数,和生命周期的函数类似。
3. 注意:
- setState是异步的!不保证数据的同步。
- setState更新状态时可能会导致页面不必要的重新渲染,影响加载。
- setState管理大量组件状态也许会导致不必要的生命周期函数钩子调用。
setState 被调用后最终会走到 scheduleUpdateOnFiber 这个函数里面来,下面是源码:
function scheduleUpdateOnFiber(fiber, expirationTime) {
checkForNestedUpdates();
warnAboutRenderPhaseUpdatesInDEV(fiber);
var root = markUpdateTimeFromFiberToRoot(fiber, expirationTime);
if (root === null) {
warnAboutUpdateOnUnmountedFiberInDEV(fiber);
return;
}
checkForInterruption(fiber, expirationTime);
recordScheduleUpdate(); // TODO: computeExpirationForFiber also reads the priority. Pass the
// priority as an argument to that function and this one.
var priorityLevel = getCurrentPriorityLevel();
if (expirationTime === Sync) {
if ( // Check if we're inside unbatchedUpdates
(executionContext & LegacyUnbatchedContext) !== NoContext && // Check if we're not already rendering
(executionContext & (RenderContext | CommitContext)) === NoContext) {
// Register pending interactions on the root to avoid losing traced interaction data.
schedulePendingInteractions(root, expirationTime); // This is a legacy edge case. The initial mount of a ReactDOM.render-ed
// root inside of batchedUpdates should be synchronous, but layout updates
// should be deferred until the end of the batch.
performSyncWorkOnRoot(root);
} else {
ensureRootIsScheduled(root);
schedulePendingInteractions(root, expirationTime);
// 重点!!!!!!
if (executionContext === NoContext) {
// Flush the synchronous work now, unless we're already working or inside
// a batch. This is intentionally inside scheduleUpdateOnFiber instead of
// scheduleCallbackForFiber to preserve the ability to schedule a callback
// without immediately flushing it. We only do this for user-initiated
// updates, to preserve historical behavior of legacy mode.
flushSyncCallbackQueue();
}
}
} else {
ensureRootIsScheduled(root);
schedulePendingInteractions(root, expirationTime);
}
if ((executionContext & DiscreteEventContext) !== NoContext && ( // Only updates at user-blocking priority or greater are considered
// discrete, even inside a discrete event.
priorityLevel === UserBlockingPriority$1 || priorityLevel === ImmediatePriority)) {
// This is the result of a discrete event. Track the lowest priority
// discrete update per root so we can flush them early, if needed.
if (rootsWithPendingDiscreteUpdates === null) {
rootsWithPendingDiscreteUpdates = new Map([[root, expirationTime]]);
} else {
var lastDiscreteTime = rootsWithPendingDiscreteUpdates.get(root);
if (lastDiscreteTime === undefined || lastDiscreteTime > expirationTime) {
rootsWithPendingDiscreteUpdates.set(root, expirationTime);
}
}
}
}
我们着重看这段代码:
if (executionContext === NoContext) {
// Flush the synchronous work now, unless we're already working or inside
// a batch. This is intentionally inside scheduleUpdateOnFiber instead of
// scheduleCallbackForFiber to preserve the ability to schedule a callback
// without immediately flushing it. We only do this for user-initiated
// updates, to preserve historical behavior of legacy mode.
flushSyncCallbackQueue();
}
executionContext 代表了目前 react 所处的阶段,而 NoContext 你可以理解为是 react 已经没活干了的状态。而 flushSyncCallbackQueue 里面就会去同步调用我们的 this.setState ,也就是说会同步更新我们的 state 。所以,我们知道了,当 executionContext 为 NoContext 的时候,我们的 setState 就是同步的。那什么地方会改变 executionContext 的值呢?
我们随便找几个地方看看
function batchedEventUpdates$1(fn, a) {
var prevExecutionContext = executionContext;
executionContext |= EventContext;
...省略
}
function batchedUpdates$1(fn, a) {
var prevExecutionContext = executionContext;
executionContext |= BatchedContext;
...省略
}
当 react 进入它自己的调度步骤时,会给这个 executionContext 赋予不同的值,表示不同的操作以及当前所处的状态,而 executionContext 的初始值就是 NoContext ,所以只要你不进入 react 的调度流程,这个值就是 NoContext ,那你的 setState 就是同步的。