本文内容均针对于18.x以下版本
setState
到底是同步还是异步?很多人可能都有这种经历,面试的时候面试官给了你一段代码,让你说出输出的内容,比如这样:
constructor(props) {super(props);this.state = {val: '0'}}componentDidMount() {this.setState({data: '1'})console.log("val: ", this.state.val);// val: 0setTimeout(() => {this.setState({data: '2'})console.log("setTimeout ", this.state.val);//val:2})}
而这段代码的输出结果,第一个 console.log
会输出0 ,而第二个 console.log
会输出2 。也就是第一次 setState
的时候,它是异步的,第二次 setState
的时候,它又变成了同步的。
这是为什么呢?
只要你进入了
react
的调度流程,那就是异步的。只要你没有进入react
的调度流程,那就是同步的。什么东西不会进入react
的调度流程?setTimeout
setInterval
,直接在DOM
上绑定原生事件等。这些都不会走React
的调度流程,你在这种情况下调用setState
,那这次setState
就是同步的。 否则就是异步的。而
setState
同步执行的情况下,DOM
也会被同步更新,也就意味着如果你多次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
就是同步的。