setState同步还是异步?
本文基于React17.0.0
setState的执行流程
简单来说,setState
函数是React.Component
类的一个方法,它记录了当前应用发生的更新update
,并把这些update
用一个队列updateQueue
记录下来,然后进入scheduleUpdateOnFiber
这个函数中进行调度更新,然后触发performSyncWorkOnRoot
函数来进入render
阶段。最后在commit
阶段把更新渲染到dom上。
同步还是异步
为了更加具体的理解setState
的执行行为,我们先来看下面的代码,预设我们是在ReactDOM.render
的legacy
模式下运行,你可以正确说出两次console.log
的结果吗?
constructor(props) {
super(props);
this.state = {
data: "hello"
}
}
componentDidMount() {
//第一个setState
this.setState({
data: 'world'
})
console.log("in componentDidMount: ", this.state.data);
setTimeout(() => {
// 第二个setState
this.setState({
data: 'Bob'
})
console.log("in setTimeout: ", this.state.data);
})
}
揭晓答案,结果是 我们可以看到,第一个
log
输出的是hello
,也就是说,当执行第一个setState
时,是表现为同步行为的,在执行第二个setState
时,log输出的是Bob,是表现为异步行为的。
那么这是为什么呢,可以粗略看出,两个setState
执行的环境是不同的,第一个setState
的执行环境是在componentDidMount()
这个生命周期钩子函数中,第二个setState
是在setTimeout
中执行的。相信大家一眼就看出来了,问题的本质就是执行环境的不同,导致了setState
在执行时的上下文就不一样了,而执行上下文executionContext
,就是决定setState同步异步行为的关键。
为何executionContext
为何在这个过程中这么重要呢,我们在前面了解到setState
在执行过程中会进入 scheduleUpdateOnFiber
这个函数,我们先来简单看下在React
源码中,有什么蹊跷吧。
scheduleUpdateOnFiber
function scheduleUpdateOnFiber(fiber, lane, eventTime) {
// 省略 .....
if (lane === SyncLane) {// ReactDOM.render 同步执行
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, lane); // This is a legacy edge case. The initial mount of a ReactDOM.render-ed
performSyncWorkOnRoot(root);
} else {
ensureRootIsScheduled(root, eventTime);
schedulePendingInteractions(root, lane);
if (executionContext === NoContext) {
resetRenderTimer();
flushSyncCallbackQueue();
}
}
} else {
// 省略 .....
}
ensureRootIsScheduled(root, eventTime);
schedulePendingInteractions(root, lane);
}
mostRecentlyUpdatedRoot = root;
}
我们可以看到其中的关键代码
if (executionContext === NoContext) {
resetRenderTimer();
flushSyncCallbackQueue();
}
其中flushSyncCallbackQueue
函数的作用就是同步执行更新队列里的更新update
,前面我们说过了setState
后会把更新对象update
以队列形式保存下来,这里就是要执行这些更新并清空队列。我们可以看出,如果当前的执行上下文executionContext
是NoContext
,也就是说明React
已经不处于自己的调度环节了,而是处于一种无事可做的状态时,React
就会去同步的执行setState
的回调函数进行更新。那么,什么时候才会是一种无事可做的状态呢?
答案就是不处于React本身的调度阶段时,比如setTimeout
,网络请求,直接在Dom
节点上绑定的事件等,这些行为都不会触发React的调度行为。当React处于自己的调度阶段时,会根据所处的状态不同给executionContext
赋值不同的值,如赋值为BatchedContext
时说明进入了更新合并阶段,而executionContext
默认情况下就是NoContext
。所以不在调度阶段时,React
就会进入无事可做的状态,就会将setState
同步执行。在React
处于自己的调度阶段,会执行诸如生命周期钩子函数,合成事件等,在这些情况下,会触发batchedUpdate
进行合并更新,所以此时将executionContext
赋值为BatchedContext
,那么自然就是异步的行为了。
在我们的例子中,componentDidMount
属于React
调度流程的一部分,所以其中的setState
会被异步执行,componentDidMount
执行完后,React
就退出调度过程了,此时的executionContext
是 NoContext
了,但是我们的代码还没有执行完,因为setTimeout
是一个异步方法,等它的回调被执行的时候,React
就会以同步的方式执行setState
了。
setState与useState
useState是React为函数组件赋予状态的一种做法,但是它和setState的调用栈有重合的地方,也就是都会进入scheduleUpdateOnFiber函数,所以在同步异步的行为上是没有区别的。
转载自:https://segmentfault.com/a/1190000041724675