詳細(xì)談?wù)凴eact中setState是一個(gè)宏任務(wù)還是微任務(wù)
前言
最近有個(gè)朋友面試,面試官問(wèn)了個(gè)奇葩的問(wèn)題,也就是我寫(xiě)在標(biāo)題上的這個(gè)問(wèn)題。
能問(wèn)出這個(gè)問(wèn)題,面試官應(yīng)該對(duì) React 不是很了解,也是可能是看到面試者簡(jiǎn)歷里面有寫(xiě)過(guò)自己熟悉 React,面試官想通過(guò)這個(gè)問(wèn)題來(lái)判斷面試者是不是真的熟悉 React 🤣。
面試官的問(wèn)法是否正確?§
面試官的問(wèn)題是,setState 是一個(gè)宏任務(wù)還是微任務(wù),那么在他的認(rèn)知里,setState 肯定是一個(gè)異步操作。為了判斷 setState 到底是不是異步操作,可以先做一個(gè)實(shí)驗(yàn),通過(guò) CRA 新建一個(gè) React 項(xiàng)目,在項(xiàng)目中,編輯如下代碼:
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>我的關(guān)注人數(shù):{this.state.count}</p> </div> ); } } export default App;
頁(yè)面大概長(zhǎng)這樣:
上面的 React Logo 綁定了一個(gè)點(diǎn)擊事件,現(xiàn)在需要實(shí)現(xiàn)這個(gè)點(diǎn)擊事件,在點(diǎn)擊 Logo 之后,進(jìn)行一次 setState 操作,在 set 操作完成時(shí)打印一個(gè) log,并且在 set 操作之前,分別添加一個(gè)宏任務(wù)和微任務(wù)。代碼如下:
handleClick = () => { const fans = Math.floor(Math.random() * 10) setTimeout(() => { console.log('宏任務(wù)觸發(fā)') }) Promise.resolve().then(() => { console.log('微任務(wù)觸發(fā)') }) this.setState({ count: this.state.count + fans }, () => { console.log('新增粉絲數(shù):', fans) }) }
很明顯,在點(diǎn)擊 Logo 之后,先完成了 setState 操作,然后再是微任務(wù)的觸發(fā)和宏任務(wù)的觸發(fā)。所以,setState 的執(zhí)行時(shí)機(jī)是早于微任務(wù)與宏任務(wù)的,即使這樣也只能說(shuō)它的執(zhí)行時(shí)機(jī)早于 Promise.then,還不能證明它就是同步任務(wù)。
handleClick = () => { const fans = Math.floor(Math.random() * 10) console.log('開(kāi)始運(yùn)行') this.setState({ count: this.state.count + fans }, () => { console.log('新增粉絲數(shù):', fans) }) console.log('結(jié)束運(yùn)行') }
這么看,似乎 setState 又是一個(gè)異步的操作。主要原因是,在 React 的生命周期以及綁定的事件流中,所有的 setState 操作會(huì)先緩存到一個(gè)隊(duì)列中,在整個(gè)事件結(jié)束后或者 mount 流程結(jié)束后,才會(huì)取出之前緩存的 setState 隊(duì)列進(jìn)行一次計(jì)算,觸發(fā) state 更新。只要我們跳出 React 的事件流或者生命周期,就能打破 React 對(duì) setState 的掌控。最簡(jiǎn)單的方法,就是把 setState 放到 setTimeout 的匿名函數(shù)中。
handleClick = () => { setTimeout(() => { const fans = Math.floor(Math.random() * 10) console.log('開(kāi)始運(yùn)行') this.setState({ count: this.state.count + fans }, () => { console.log('新增粉絲數(shù):', fans) }) console.log('結(jié)束運(yùn)行') }) }
由此可見(jiàn),setState 本質(zhì)上還是在一個(gè)事件循環(huán)中,并沒(méi)有切換到另外宏任務(wù)或者微任務(wù)中,在運(yùn)行上是基于同步代碼實(shí)現(xiàn),只是行為上看起來(lái)像異步。所以,根本不存在面試官的問(wèn)題。
React 是如何控制 setState 的 ?§
前面的案例中,setState 只有在 setTimeout 中才會(huì)變得像一個(gè)同步方法,這是怎么做到的?
handleClick = () => { // 正常的操作 this.setState({ count: this.state.count + 1 }) } handleClick = () => { // 脫離 React 控制的操作 setTimeout(() => { this.setState({ count: this.state.count + fans }) }) }
先回顧之前的代碼,在這兩個(gè)操作中,我們分別在 Performance 中記錄一次調(diào)用棧,看看兩者的調(diào)用棧有何區(qū)別。
在調(diào)用棧中,可以看到 Component.setState 方法最終會(huì)調(diào)用enqueueSetState 方法 ,而 enqueueSetState 方法內(nèi)部會(huì)調(diào)用 scheduleUpdateOnFiber 方法,區(qū)別就在于正常調(diào)用的時(shí)候,scheduleUpdateOnFiber 方法內(nèi)只會(huì)調(diào)用 ensureRootIsScheduled ,在事件方法結(jié)束后,才會(huì)調(diào)用 flushSyncCallbackQueue 方法。而脫離 React 事件流的時(shí)候,scheduleUpdateOnFiber 在 ensureRootIsScheduled 調(diào)用結(jié)束后,會(huì)直接調(diào)用 flushSyncCallbackQueue 方法,這個(gè)方法就是用來(lái)更新 state 并重新進(jìn)行 render。
function scheduleUpdateOnFiber(fiber, lane, eventTime) { if (lane === SyncLane) { // 同步操作 ensureRootIsScheduled(root, eventTime); // 判斷當(dāng)前是否還在 React 事件流中 // 如果不在,直接調(diào)用 flushSyncCallbackQueue 更新 if (executionContext === NoContext) { flushSyncCallbackQueue(); } } else { // 異步操作 } }
上述代碼可以簡(jiǎn)單描述這個(gè)過(guò)程,主要是判斷了 executionContext 是否等于 NoContext 來(lái)確定當(dāng)前更新流程是否在 React 事件流中。
眾所周知,React 在綁定事件時(shí),會(huì)對(duì)事件進(jìn)行合成,統(tǒng)一綁定到 document 上( react@17 有所改變,變成了綁定事件到 render 時(shí)指定的那個(gè) DOM 元素),最后由 React 來(lái)派發(fā)。
所有的事件在觸發(fā)的時(shí)候,都會(huì)先調(diào)用 batchedEventUpdates$1 這個(gè)方法,在這里就會(huì)修改 executionContext 的值,React 就知道此時(shí)的 setState 在自己的掌控中。
// executionContext 的默認(rèn)狀態(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ì)上都是同步的,只是有個(gè)先后順序的問(wèn)題。
未來(lái)會(huì)有異步的 setState§
如果你有認(rèn)真看上面的代碼,你會(huì)發(fā)現(xiàn)在 scheduleUpdateOnFiber 方法內(nèi),會(huì)判斷 lane 是否為同步,那么是不是存在異步的情況?
function scheduleUpdateOnFiber(fiber, lane, eventTime) { if (lane === SyncLane) { // 同步操作 ensureRootIsScheduled(root, eventTime); // 判斷當(dāng)前是否還在 React 事件流中 // 如果不在,直接調(diào)用 flushSyncCallbackQueue 更新 if (executionContext === NoContext) { flushSyncCallbackQueue(); } } else { // 異步操作 } }
React 在兩年前,升級(jí) fiber 架構(gòu)的時(shí)候,就是為其異步化做準(zhǔn)備的。在 React 18 將會(huì)正式發(fā)布 Concurrent 模式,關(guān)于 Concurrent 模式,官方的介紹如下。
什么是 Concurrent 模式?
Concurrent 模式是一組 React 的新功能,可幫助應(yīng)用保持響應(yīng),并根據(jù)用戶的設(shè)備性能和網(wǎng)速進(jìn)行適當(dāng)?shù)恼{(diào)整。在 Concurrent 模式中,渲染不是阻塞的。它是可中斷的。這改善了用戶體驗(yàn)。它同時(shí)解鎖了以前不可能的新功能。
現(xiàn)在如果想使用 Concurrent 模式,需要使用 React 的實(shí)驗(yàn)版本。如果你對(duì)這部分內(nèi)容感興趣可以閱讀我之前的文章:https://blog.shenfq.com/posts/2020/React%20架構(gòu)的演變%20-%20從同步到異步.html
總結(jié)
到此這篇關(guān)于React中setState是一個(gè)宏任務(wù)還是微任務(wù)的文章就介紹到這了,更多相關(guān)React setState是宏任務(wù)還是微任務(wù)內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
React踩坑之a(chǎn)ntd輸入框rules中的required=true問(wèn)題
這篇文章主要介紹了React踩坑之a(chǎn)ntd輸入框rules中的required=true問(wèn)題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2023-06-06React函數(shù)式組件Hook中的useEffect函數(shù)的詳細(xì)解析
useEffect是react v16.8新引入的特性。我們可以把useEffect hook看作是componentDidMount、componentDidUpdate、componentWillUnmounrt三個(gè)函數(shù)的組合2022-10-10從零開(kāi)始搭建一個(gè)react項(xiàng)目開(kāi)發(fā)
這篇文章主要介紹了從零開(kāi)始搭建一個(gè)react項(xiàng)目開(kāi)發(fā),小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2018-02-02React報(bào)錯(cuò)信息之Expected?an?assignment?or?function?call?and?
這篇文章主要介紹了React報(bào)錯(cuò)之Expected?an?assignment?or?function?call?and?instead?saw?an?expression,下面有兩個(gè)示例來(lái)展示錯(cuò)誤是如何產(chǎn)生的,需要的朋友可以參考下2022-08-08react中的forwardRef 和memo的區(qū)別解析
forwardRef和memo是React中用于性能優(yōu)化和組件復(fù)用的兩個(gè)高階函數(shù),本文給大家介紹react中的forwardRef 和memo的區(qū)別及適用場(chǎng)景,感興趣的朋友跟隨小編一起看看吧2023-10-10create-react-app全家桶router?mobx全局安裝配置
這篇文章主要為大家介紹了create-react-app全家桶router?mobx全局安裝配置,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-06-06React Native使用Modal自定義分享界面的示例代碼
本篇文章主要介紹了React Native使用Modal自定義分享界面的示例代碼,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2017-10-10