React 中 setState 是一個宏任務還是微任務?
最近有個朋友面試,面試官問了個奇葩的問題,也就是我寫在標題上的這個問題。

能問出這個問題,面試官應該對 React 不是很了解,也是可能是看到面試者簡歷里面有寫過自己熟悉 React,面試官想通過這個問題來判斷面試者是不是真的熟悉 React ??。
面試官的問法是否正確?
面試官的問題是,setState 是一個宏任務還是微任務,那么在他的認知里,setState 肯定是一個異步操作。為了判斷 setState 到底是不是異步操作,可以先做一個實驗,通過 CRA 新建一個 React 項目,在項目中,編輯如下代碼:
import?React?from?'react';
import?logo?from?'./logo.svg';
import?'./App.css';
class?App?extends?React.Component?{
??state?=?{
????count:?1000
??}
??render()?{
????return?(
??????<div?className="App">
????????<img
??????????src={logo}?alt="logo"
??????????className="App-logo"
??????????onClick={this.handleClick}
????????/>
????????<p>我的關注人數(shù):{this.state.count}</p>
??????</div>
????);
??}
}
export?default?App;
頁面大概長這樣:

上面的 React Logo 綁定了一個點擊事件,現(xiàn)在需要實現(xiàn)這個點擊事件,在點擊 Logo 之后,進行一次 setState 操作,在 set 操作完成時打印一個 log,并且在 set 操作之前,分別添加一個宏任務和微任務。代碼如下:
handleClick?=?()?=>?{
??const?fans?=?Math.floor(Math.random()?*?10)
??setTimeout(()?=>?{
????console.log('宏任務觸發(fā)')
??})
??Promise.resolve().then(()?=>?{
????console.log('微任務觸發(fā)')
??})
??this.setState({
????count:?this.state.count?+?fans
??},?()?=>?{
????console.log('新增粉絲數(shù):',?fans)
??})
}

很明顯,在點擊 Logo 之后,先完成了 setState 操作,然后再是微任務的觸發(fā)和宏任務的觸發(fā)。所以,setState 的執(zhí)行時機是早于微任務與宏任務的,即使這樣也只能說它的執(zhí)行時機早于 Promise.then,還不能證明它就是同步任務。
handleClick?=?()?=>?{
??const?fans?=?Math.floor(Math.random()?*?10)
??console.log('開始運行')
??this.setState({
????count:?this.state.count?+?fans
??},?()?=>?{
????console.log('新增粉絲數(shù):',?fans)
??})
??console.log('結(jié)束運行')
}

這么看,似乎 setState 又是一個異步的操作。主要原因是,在 React 的生命周期以及綁定的事件流中,所有的 setState 操作會先緩存到一個隊列中,在整個事件結(jié)束后或者 mount 流程結(jié)束后,才會取出之前緩存的 setState 隊列進行一次計算,觸發(fā) state 更新。只要我們跳出 React 的事件流或者生命周期,就能打破 React 對 setState 的掌控。最簡單的方法,就是把 setState 放到 setTimeout 的匿名函數(shù)中。
handleClick?=?()?=>?{
??setTimeout(()?=>?{
????const?fans?=?Math.floor(Math.random()?*?10)
????console.log('開始運行')
????this.setState({
??????count:?this.state.count?+?fans
????},?()?=>?{
??????console.log('新增粉絲數(shù):',?fans)
????})
????console.log('結(jié)束運行')
??})
}

所以,setState 就是一次同步行為,根本不存在面試官的問題。
React 是如何控制 setState 的 ?
前面的案例中,setState 只有在?setTimeout?中才會變得像一個同步方法,這是怎么做到的?
handleClick?=?()?=>?{
??//?正常的操作
??this.setState({
????count:?this.state.count?+?1
??})
}
handleClick?=?()?=>?{
??//?脫離?React?控制的操作
??setTimeout(()?=>?{
????this.setState({
??????count:?this.state.count?+?fans
????})
??})
}
先回顧之前的代碼,在這兩個操作中,我們分別在 Performance 中記錄一次調(diào)用棧,看看兩者的調(diào)用棧有何區(qū)別。
正常操作
脫離 React 控制的操作在調(diào)用棧中,可以看到 Component.setState 方法最終會調(diào)用?enqueueSetState?方法,而 enqueueSetState 方法內(nèi)部會調(diào)用 scheduleUpdateOnFiber 方法,區(qū)別就在于正常調(diào)用的時候,scheduleUpdateOnFiber 方法內(nèi)只會調(diào)用 ensureRootIsScheduled ,在事件方法結(jié)束后,才會調(diào)用 ?flushSyncCallbackQueue?方法。而脫離 React 事件流的時候,scheduleUpdateOnFiber 在 ensureRootIsScheduled 調(diào)用結(jié)束后,會直接調(diào)用 flushSyncCallbackQueue 方法,這個方法就是用來更新 state 并重新進行 render 。


function?scheduleUpdateOnFiber(fiber,?lane,?eventTime)?{
??if?(lane?===?SyncLane)?{
????//?同步操作
????ensureRootIsScheduled(root,?eventTime);
????//?判斷當前是否還在?React?事件流中
????//?如果不在,直接調(diào)用?flushSyncCallbackQueue?更新
????if?(executionContext?===?NoContext)?{
??????flushSyncCallbackQueue();
????}
??}?else?{
????//?異步操作
??}
}
上述代碼可以簡單描述這個過程,主要是判斷了 executionContext 是否等于 NoContext 來確定當前更新流程是否在 React 事件流中。
眾所周知,React 在綁定事件時,會對事件進行合成,統(tǒng)一綁定到?document?上(?react@17?有所改變,變成了綁定事件到?render?時指定的那個 DOM 元素),最后由 React 來派發(fā)。
所有的事件在觸發(fā)的時候,都會先調(diào)用?batchedEventUpdates$1?這個方法,在這里就會修改?executionContext?的值,React 就知道此時的?setState?在自己的掌控中。
//?executionContext?的默認狀態(tài)
var?executionContext?=?NoContext;
function?batchedEventUpdates$1(fn,?a)?{
??var?prevExecutionContext?=?executionContext;
??executionContext?|=?EventContext;?//?修改狀態(tài)
??try?{
????return?fn(a);
??}?finally?{
????executionContext?=?prevExecutionContext;
??//?調(diào)用結(jié)束后,調(diào)用?flushSyncCallbackQueue
????if?(executionContext?===?NoContext)?{
??????flushSyncCallbackQueue();
????}
??}
}
所以,不管是直接調(diào)用
flushSyncCallbackQueue ,還是推遲調(diào)用,這里本質(zhì)上都是同步的,只是有個先后順序的問題。未來會有異步的 setState
如果你有認真看上面的代碼,你會發(fā)現(xiàn)在 scheduleUpdateOnFiber 方法內(nèi),會判斷 lane 是否為同步,那么是不是存在異步的情況?
function?scheduleUpdateOnFiber(fiber,?lane,?eventTime)?{
??if?(lane?===?SyncLane)?{
????//?同步操作
????ensureRootIsScheduled(root,?eventTime);
????//?判斷當前是否還在?React?事件流中
????//?如果不在,直接調(diào)用?flushSyncCallbackQueue?更新
????if?(executionContext?===?NoContext)?{
??????flushSyncCallbackQueue();
????}
??}?else?{
????//?異步操作
??}
}
React 在兩年前,升級 fiber 架構(gòu)的時候,就是為其異步化做準備的。在 React 18 將會正式發(fā)布 Concurrent 模式,關于 Concurrent 模式,官方的介紹如下。

什么是 Concurrent 模式?
Concurrent 模式是一組 React 的新功能,可幫助應用保持響應,并根據(jù)用戶的設備性能和網(wǎng)速進行適當?shù)恼{(diào)整。在 Concurrent 模式中,渲染不是阻塞的。它是可中斷的。這改善了用戶體驗。它同時解鎖了以前不可能的新功能。
現(xiàn)在如果想使用 Concurrent 模式,需要使用 React 的實驗版本。如果你對這部分內(nèi)容感興趣可以閱讀我之前的文章:《React 架構(gòu)的演變 - 從同步到異步》。
