setState 是同步还是异步的?
1trigger = (isBatchedUpdate: boolean) => {
2 const runSetState = () => {
3 this.setState({ count: this.state.count + 1 }, () => console.log(this.state.count));
4 };
5
6 if (isBatchedUpdate) {
7 runSetState();
8 } else {
9 setTimeout(runSetState, 0);
10 }
11};
12
13<button onClick={() => this.trigger(true)}>触发合成事件</button>;
14<button onClick={() => this.trigger(false)}>触发 setTimeout 事件</button>;
可以发现两个执行的时机不一样,console.log
的结果也不一样。一个是同步,一个则是异步。
分析
我们来看看 react 部分源码
1function scheduleUpdateOnFiber(fiber, lane, eventTime) {
2 if (lane === SyncLane) {
3 // 同步操作
4 ensureRootIsScheduled(root, eventTime)
5 // 判断当前是否还在 React 事件流中
6 // 如果不在,直接调用 flushSyncCallbackQueue 更新
7 if (executionContext === NoContext)
8 flushSyncCallbackQueue()
9 }
10 else {
11 // 异步操作
12 }
13}
上述代码可以简单描述这个过程,主要是判断了 executionContext
是否等于 NoContext
来确定当前更新流程是否在 React 事件流中。
所有的事件在触发的时候,都会先调用 batchedEventUpdates$1
这个方法,在这里就会修改 executionContext
的值,React 就知道此时的 setState
在自己的掌控中。
1// executionContext 的默认状态
2let executionContext = NoContext
3function batchedEventUpdates$1(fn, a) {
4 const prevExecutionContext = executionContext
5 executionContext |= EventContext // 修改状态
6 try {
7 return fn(a)
8 }
9 finally {
10 executionContext = prevExecutionContext
11 // 调用结束后,调用 flushSyncCallbackQueue
12 if (executionContext === NoContext)
13 flushSyncCallbackQueue()
14 }
15}

所以,不管是直接调用 flushSyncCallbackQueue ,还是推迟调用,这里本质上都是同步的,只是有个先后顺序的问题。
结论
同步情况
- 当前是
Legacy 模式
- 在非合成事件中执行
setState
,比如 setTimeout
, Promise
, MessageChannel
等
异步情况
- 如果是合成事件中的回调,
executionContext |= EventContext
, 所以不会进入, 最终表现出异步
- concurrent 模式下都为异步
批处理
在React 合成事件中执行多次 setState 后,react 会合并进行一次更新,这样就可以提高性能,这就是批处理的概念。
1trigger = (isBatchedUpdate: boolean) => {
2 const runSetState = () => {
3 this.setState({ count: this.state.count + 1 });
4 this.setState({ age: this.state.age + 1 });
5 };
6
7 if (isBatchedUpdate) {
8 runSetState(); // render 一次
9 } else {
10 setTimeout(runSetState, 0); // render 两次
11 }
12};
即使是 async 函数,isBatchedUpdate 为 false,那多次 setState 实际上也会 render 多次。~~
给张图
