React?中的?setState?是同步還是異步
setState 是同步還是異步?肯定是異步的呀。
確定么?那看一下這段代碼會(huì)打印什么:
import { Component } from 'react'; class Dong extends Component { constructor() { super(); this.state = { count: 0 } } componentDidMount() { setTimeout(() => { this.setState({ count: 1 }); console.log(this.state.count); this.setState({ count: 2 }); console.log(this.state.count); }); } render() { console.log('render:', this.state.count); return <div>{this.state.count}</div>; } }
在 setTimeout 里修改了兩次 state,并打印了 state 的值。如果是異步的,那應(yīng)該打印的時(shí)候 count 還沒修改,依然是 0,所以打印兩次 0。然后初始化渲染一次,setState 后再渲染一次,應(yīng)該 render 兩次,count 分別為 0 和 2。按照異步的方式來(lái)分析,確實(shí)應(yīng)該是這樣的。
我們執(zhí)行一下:
會(huì)發(fā)現(xiàn)兩次打印分別是 1 和 2,也就是說(shuō) setState 同步修改了 state,然后每次都觸發(fā)了渲染,所以一共 render 3 次,分別是 0、1、2。
那這么說(shuō) setState 是同步的?確定么?那看下這段代碼會(huì)打印什么?
class Dong extends Component { constructor() { super(); this.state = { count: 0 } } componentDidMount() { this.setState({ count: 1 }); console.log(this.state.count); this.setState({ count: 2 }); console.log(this.state.count); this.setState({ count: 3 }); console.log(this.state.count); } render() { console.log('render:', this.state.count); return <div>{this.state.count}</div>; } }
如果 setState 是同步的,那執(zhí)行完就會(huì)修改 state,應(yīng)該分別打印 1、2、3,然后觸發(fā)三次 render,加上最開始的一次,一共四次,打印 0、1、2、3。
我們來(lái)執(zhí)行一下:
三次打印都是 0,這說(shuō)明 setState 是異步的。而且三次 setState 只觸發(fā)了一次 render,加上最開始的 render,一共兩次,打印 0、3。
什么鬼,怎么又是異步的了?
而且不止 class 組件的 setState 是這樣,換成 function 組件的 useState 也是一樣的:
比如修改三次 state,只會(huì) render 一次:
而在 setTimeout 里,每次修改 state 都會(huì) render:
是不是有點(diǎn)暈,什么情況下 setState 是同步的,什么情況下是異步的呢?這要從源碼找答案了,我們來(lái)讀一下 setState 的源碼。
首先理一下 React 渲染的流程:
React 渲染流程
react 通過(guò) jsx 來(lái)描述界面,jsx 可以通過(guò) babel 等編譯器編譯成 render function,然后執(zhí)行后產(chǎn)生 vdom:
這個(gè) vdom 也不是直接渲染的,而是會(huì)先轉(zhuǎn)化為 fiber,之后再渲染。因?yàn)?vdom 里每個(gè)節(jié)點(diǎn)只記錄了子節(jié)點(diǎn)(children),沒有記錄兄弟節(jié)點(diǎn),所以必須一次性渲染完,不能打斷。而轉(zhuǎn)成 fiber 的鏈表結(jié)構(gòu)就會(huì)記錄父節(jié)點(diǎn)(return)、子節(jié)點(diǎn)(child)、兄弟節(jié)點(diǎn)(sibling),就變成了可打斷的。
這里的 vdom 是 React Element 對(duì)象:
轉(zhuǎn)化為 fiber 之后是 FiberNode 的對(duì)象:
從 vdom 轉(zhuǎn)換成 fiber 的過(guò)程就叫做 reconcile,轉(zhuǎn)換過(guò)程中會(huì)順便創(chuàng)建對(duì)應(yīng)的 dom 元素,然后全部轉(zhuǎn)換完后一次性 commit 到 dom。這個(gè)過(guò)程不是一次性的,是通過(guò) scheduler 調(diào)度執(zhí)行的,那也就可以分批次進(jìn)行,也就是可打斷的含義。這就是 React 的 fiber 架構(gòu)下的渲染流程。理論說(shuō)完了,我們來(lái)對(duì)應(yīng)到源碼看一下(這里看的是 v17 的源碼):
react 把 schedule 和 reconcile 叫做 render 階段,這個(gè)階段就是把 vdom 轉(zhuǎn)為 fiber。(schedule 只是讓 reconcile 可以分多次執(zhí)行,可以打斷,但做的事情是不變的,所以 schedule 也是 render 階段的一部分)
之后把 fiber 更新到 dom 的過(guò)程就叫做 commit 階段。
對(duì)應(yīng)到源碼里就是這樣的:
這個(gè) performSyncWorkOnRoot 就是渲染的入口,就像之前所說(shuō)的,會(huì)先執(zhí)行 render 階段,把 vdom 轉(zhuǎn)成 fbier,然后再執(zhí)行 commit,更新到 dom。
render 階段會(huì)執(zhí)行一個(gè)調(diào)度的 loop:
這個(gè) loop 就是不斷地處理一個(gè)個(gè) fiber 的 reconcile:
每個(gè)節(jié)點(diǎn)都有 beginWork 和 completeWork 兩個(gè)階段,因?yàn)橐?vdom 轉(zhuǎn) fiber,而 vdom 是一個(gè)樹形結(jié)構(gòu),需要遞歸處理:
具體不同節(jié)點(diǎn)的 reconcile 邏輯不同:
比如函數(shù)組件會(huì)被調(diào)用,拿到 render 出的 vdom 繼續(xù)進(jìn)行 reconcile:
比如 class 組件會(huì)創(chuàng)建實(shí)例,調(diào)用 render 方法,拿到 vdom,然后再繼續(xù) renconcileChildren。
總之,vdom 轉(zhuǎn) fiber 是一個(gè)遞歸進(jìn)行的過(guò)程。之后再進(jìn)行 commit 階段。整個(gè)渲染流程的入口就是 performSyncWorkOnRoot 函數(shù)。渲染的流程講完了,接下來(lái)就是 setState 怎么觸發(fā)渲染的流程了:
setState 的流程
我們知道了渲染的入口就是 performSyncWorkOnRoot 函數(shù),那 setState 修改完?duì)顟B(tài),觸發(fā)一下這個(gè)函數(shù)不就行了?
確實(shí)是這樣的。setState 會(huì)調(diào)用 dispathAction,創(chuàng)建一個(gè) update 對(duì)象放到 fiber 節(jié)點(diǎn)的 updateQueue 上,然后調(diào)度渲染:
調(diào)度更新自然就是調(diào)度上面說(shuō)的那個(gè) performSyncWorkOnRoot 函數(shù):react 會(huì)先從觸發(fā) update 的 fiber 往上找到根 fiber 節(jié)點(diǎn),然后再調(diào)用 performSyncWorkOnRoot 的函數(shù)進(jìn)行渲染:
這就是 setState 之后觸發(fā)重新渲染的實(shí)現(xiàn)。而 setState 是同步還是異步,也就是在這一段控制的。我們看到判斷條件里有個(gè) excutionContext,這個(gè)是用來(lái)標(biāo)識(shí)當(dāng)前環(huán)境的,比如是批量還是非批量,是否執(zhí)行過(guò) render 階段、commit 階段。
其實(shí)在 ReactDOM.render 執(zhí)行的時(shí)候會(huì)先調(diào)用 unbatchUpdates 函數(shù):
這個(gè)函數(shù)會(huì)在 excutionContext 中設(shè)置一個(gè) unbatach 的 flag:
這樣在 update 的時(shí)候,就會(huì)立刻執(zhí)行 performSyncWorkOnRoot 來(lái)渲染。因?yàn)槭状武秩镜臅r(shí)候是馬上就要渲染的,沒必要調(diào)度。
之后走到 commit 階段會(huì)設(shè)置一個(gè) commit 的 flag:
然后再次 setState 就不會(huì)走到 unbatch 的分支了。那為什么 setTimeout 里面的 setState 會(huì)同步執(zhí)行呢?
因?yàn)橹苯訌?setTimeout 執(zhí)行的異步代碼是沒有設(shè)置 excutionContext 的,那就會(huì)走到 NoContext 的分支,會(huì)立刻渲染。
(這里的 flush 最終會(huì)調(diào)用 performSyncWorkOnRoot 函數(shù)來(lái)渲染):
有什么辦法能讓 setTimeout 里執(zhí)行的函數(shù)也有 excutionContext 呢?其實(shí) react17 暴露了 batchUpdates 的 api,用它包裹下,里面的 setState 就會(huì)批量執(zhí)行了:
它的源碼其實(shí)就是設(shè)置了下 excutionContext:
這樣等 setState 全部執(zhí)行完之后再 flush,調(diào)用 peformSyncWorkOnRoot 渲染,效果就是批量的 setState 了。其實(shí)按理來(lái)說(shuō) setState 不能叫異步,還是在同一個(gè)調(diào)用棧執(zhí)行的,只不過(guò)順序不同而已。只能叫批量還是非批量。
在 react17 中是這么處理的,如果是 react18,使用 createRoot 的 api 的話,就不會(huì)有這種問(wèn)題了,就算是 setTimeout 里的代碼也能批量執(zhí)行,
而且為了兼容 react17 這種情況,還做了特殊處理,當(dāng)沒有開啟并發(fā)模式,也就是還是用 ReactDOM.render 的 api 時(shí),沒有指定 excutionContext 還會(huì)立刻渲染:
等 react 18 普及以后,所有的 setState 都是批量的了,就不會(huì)再有批量還是非批量的問(wèn)題。
總結(jié)
雖然我們討論的是 setState 的同步異步,但這個(gè)不是 setTimeout、Promise 那種異步,只是指 setState 之后是否 state 馬上變了,是否馬上 render。
我們梳理了下 React 的渲染流程,包括 render 階段、commit 階段,render 階段是從 vdom 轉(zhuǎn) fiber,包含 schedule 和 reconcile,commit 階段是把 fiber 更新到 dom。渲染流程的入口是 performSyncWorkOnRoot 函數(shù)。setState 會(huì)創(chuàng)建 update 對(duì)象掛到 fiber 對(duì)象上,然后調(diào)度 performSyncWorkOnRoot 重新渲染。
在 react17 中,setState 是批量執(zhí)行的,因?yàn)閳?zhí)行前會(huì)設(shè)置 executionContext。但如果在 setTimeout、事件監(jiān)聽器等函數(shù)里,就不會(huì)設(shè)置 executionContext 了,這時(shí)候 setState 會(huì)同步執(zhí)行??梢栽谕饷姘粚?batchUpdates 函數(shù),手動(dòng)設(shè)置下 excutionContext 來(lái)切換成異步批量執(zhí)行。
在 react18 里面,如果用 createRoot 的 api,就不會(huì)有這種問(wèn)題了。setState 是同步還是異步這個(gè)問(wèn)題等 react18 普及以后就不會(huì)再有了,因?yàn)樗械?setState 都是異步批量執(zhí)行了。
到此這篇關(guān)于React 中的 setState 是同步還是異步的文章就介紹到這了,更多相關(guān)React setState 內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
React和Vue組件更新的實(shí)現(xiàn)及區(qū)別
React 和 Vue 都是當(dāng)今最流行的前端框架,它們都實(shí)現(xiàn)了組件化開發(fā)模式,本文將從React和Vue的組件更新原理入手,剖析兩者虛擬DOM difer算法的異同點(diǎn),具有一定的參考價(jià)值,感興趣的可以了解一下2024-02-02React Native如何消除啟動(dòng)時(shí)白屏的方法
本篇文章主要介紹了React Native如何消除啟動(dòng)時(shí)白屏的方法,詳細(xì)的介紹了解決的方法,具有一定的參考價(jià)值,有興趣的可以了解一下2017-08-08React引入antd-mobile+postcss搭建移動(dòng)端
本文給大家分享React引入antd-mobile+postcss搭建移動(dòng)端的詳細(xì)流程,文末給大家分享我的一些經(jīng)驗(yàn)記錄使用antd-mobile時(shí)發(fā)現(xiàn)我之前配置的postcss失效了,防止大家踩坑,特此把解決方案分享到腳本之家平臺(tái),需要的朋友參考下吧2021-06-06react-native-video實(shí)現(xiàn)視頻全屏播放的方法
這篇文章主要介紹了react-native-video實(shí)現(xiàn)視頻全屏播放的方法,小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2018-03-03手把手教你從零開始react+antd搭建項(xiàng)目
本文將從最基礎(chǔ)的項(xiàng)目搭建開始講起,做一個(gè)基于react和antd的后臺(tái)管理系統(tǒng),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2021-06-06react配合antd組件實(shí)現(xiàn)的管理系統(tǒng)示例代碼
這篇文章主要介紹了react配合antd組件實(shí)現(xiàn)的管理系統(tǒng)示例代碼,小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2018-04-04React?中使用?react-i18next?國(guó)際化的過(guò)程(react-i18next?的基本用法)
i18next?是一款強(qiáng)大的國(guó)際化框架,react-i18next?是基于?i18next?適用于?React?的框架,本文介紹了?react-i18next?的基本用法,如果更特殊的需求,文章開頭的官方地址可以找到答案2023-01-01詳解在React中跨組件分發(fā)狀態(tài)的三種方法
這篇文章主要介紹了詳解在React中跨組件分發(fā)狀態(tài)的三種方法,小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2018-08-08