詳細談談React中setState是一個巨集任務還是微任務
目錄
- 前言
- 面試官的問法是否正確?
- React 是如何控制 setState 的 ?
- 未來會有非同步的 setState
- 總結
前言
最近有個朋友面試,面試官問了個奇葩的問題,也就是我寫在標題上的這個問題。
能問出這個問題,面試官應該對 React 不是很瞭解,也是可能是看到面試者簡歷裡面有寫過自己熟悉 React,面試官想通過這個問題來判斷面試者是不是真的熟悉 React 🤣。
面試官的問法是否正確?
面試官的問題是,setState 是一個巨集任務還是微任務,那麼在他的認知裡,setState 肯定是一個非同步操作。為了判斷 setState 到底是不是非同步操作,可以先做一個實驗,通過 CRA 新建一個 React 專案,在專案中,編輯如下程式碼:
import React from 'react'; import logo from './logo.svg'; import './App.'; 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>我的關注人數:{this.state.count}</p> </div> ); } } export default App;
頁面大概長這樣:
上面的 React Logo 綁定了一個點選事件,現在需要實現這個點選事件,在點選 Logo 之後,進行一次 setState 操作,在 set 操作完成時列印一個 log,並且在 set 操作之前,分別新增一個巨集任務和微任務。程式碼如下:
handleClick = () => { const fans = Math.floor(Math.random() * 10) setTimeout(() => { console.log('巨集任務觸發') }) Promise.resolve().then(() => { console.log('微任務觸發') }) this.setState({ count: this.state.count + fans },() => { console.log('新增粉絲數:',fans) }) }
很明顯,在點選 Logo 之後,先完成了 setState 操作,然後再是微任務的觸發和巨集任務的觸發。所以,setState 的執行時機是早於微任務與巨集任務的,即使這樣也只能說它的執行時機早於 Promise.then,還不能證明它就是同步任務。
handleClick = () => { const fans = Math.floor(Math.random() * 10) console.log('開始執行') this.setState({ count: this.state.count + fans },fans) }) console.log('結束執行') }
這麼看,似乎 setState 又是一個非同步的操作。主要原因是,在 React 的生命週期以及繫結的事件流中,所有的 setState 操作會先快取到一個佇列中,在整個事件結束後或者 mount 流程結束後,才會取出之前快取的 setState 佇列進行一次計算,觸發 state 更新。只要我們跳出 React 的事件流或者生命週期,就能打破 React 對 setState 的掌控。最簡單的方法,就是把 setState 放到 setTimeout 的匿名函式中。
handleClick = () => { setTimeout(() => { const fans = Math.floor(Math.random() * 10) console.log('開始執行') this.setState({ count: this.state.count + fans },() => { console.log('新增粉絲數:',fans) }) console.log('結束執行') }) }
由此可見,setState 本質上還是在一個事件迴圈中,並沒有切換到另外巨集任務或者微任務中,在執行上是基於同步程式碼實現,只是行為上看起來像非同步。所以,根本不存在面試官的問題。
React 是如何控制 setState 的 ?
前面的案例中,setState 只有在 setTimeout 中才會變得像一個同步方法,這是怎麼做到的?
handleClick = () => { // 正常的操作 this.setState({ count: this.state.count + 1 }) } handleClick = () => { // 脫離 React 控制的操作 setTimeout(() => { this.setState({ count: this.state.count + fans }) }) }
先回顧之前的程式碼,在這兩個操作中,我們分別在 Performance 中記錄一次呼叫棧,看看兩者的呼叫棧有何區別。
在呼叫棧中,可以看到 Component.setState 方法最終會呼叫enqueueSetState 方法 ,而 enqueueSetState 方法內部會呼叫 scheduleUpdateOnFiber 方法,區別就在於正常呼叫的時候,scheduleUpdateOnFiber 方法內只會呼叫 ensureRootIsScheduled ,在事件方法結束後,才會呼叫 flushSyncCallbackQueue 方法。而脫離 React 事件流的時候,scheduleUpdateOnFiber 在 ensureRootIsScheduled 呼叫結束後,會直接呼叫 flushSyncCallbackQueue 方法,這個方法就是用來更新 state 並重新進行 render。
function scheduleUpdateOnFiber(fiber,lane,eventTime) { if (lane === SyncLane) { // 同步操作 ensureRootIsScheduled(root,eventTime); // 判斷當前是否還在 React 事件流中 // 如果不在,直接呼叫 flushSyncCallbackQueue 更新 if (executionContext === NoContext) { flushSyncCallbackQueue(); } } else { // 非同步操作 } }
上述程式碼可以簡單描述這個過程,主要是判斷了 executionContext 是否等於 NoContext 來確定當前更新流程是否在 React 事件流中。
眾所周知,React 在繫結事件時,會對事件進行合成,統一繫結到 document 上( react@17 有所改變,變成了繫結事件到 render 時指定的那個 DOM 元素),最後由 React 來派發。
所有的事件在觸發的時候,都會先呼叫 batchedEventUpdates$1 這個方法,在這裡就會修改 executionContext 的值,React 就知道此時的 setState 在自己的掌控中。
// executionContext 的預設狀態
var ewww.cppcns.comxecutionContext = NoContext;
function batchedEventUpdates$1(fn,a) {
var prevExecutionContext = executionContext;
executionContext |= EventContext; // 修改狀態
try {
return fn(a);
} finally {
executionContext = prevExecutionContext;
// 呼叫結束後,呼叫 flushSyncCallbackQueue
if (executionContext === NoContext) {
flushSyncCallbackQueue();
}
}
}
所以,不管是直接呼叫 flushSyncCallbackQueue ,還是推遲呼叫,這裡本質上都是同步的,只是有個先後順序的問題。
未來會有非同步的 setState
如果你有認真看上面的程式碼,你會發現在 scheduleUpdateOnFiber 方法內,會判斷 lane 是否為同步,那麼是不是存在非同步的情況?
function scheduleUpdateOnFiber(fiber,eventTime) { if (lane === SyncLane) { // 同步操作 ensureRootIsScheduled(root,eventTime); // 判斷當前是否還在 React 事件流中 // 如果不在,直接呼叫 flushSyncCallbackQueue 更新 if (executionContext === NoContext) { flushSyncCallbackQueue(); } } else { // 非同步操作 } }
React 在兩年前,升級 fiber 架構的時候,就是為其非同步化做準備的。在 React 18 將會正式釋出 Concurrent 模式,關於 Concurrent 模式,官方的介紹如下。
什麼是 Concurrent 模式?
Concurrent 模式是一組 React 的新功能,可幫助應用保持響應,並根據使用者的裝置效能和網速進行適當的調整。在 Concurrent 模式中,渲染不是阻塞的。它是可中斷的。這改善了使用者體驗。它同時解鎖了以前不可能的新功能。
現在如果想使用 Concurrent 模式,需要使用 React 的實驗版本。如果你對這部分內容感興趣可以閱讀我之前的文章:https://blog.shenfq.com/posts/2020/React%20架構的演變%20-%20從同步到非同步.html
總結
到此這篇關於React中setState是一個巨集任務還是微任務的文章就介紹到這了,更多相關React setState是巨集任務還是微任務內容請搜尋我們以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援我們!