新聞中心
大家好,我是卡頌。

網(wǎng)站建設(shè)哪家好,找創(chuàng)新互聯(lián)!專注于網(wǎng)頁(yè)設(shè)計(jì)、網(wǎng)站建設(shè)、微信開發(fā)、重慶小程序開發(fā)、集團(tuán)企業(yè)網(wǎng)站建設(shè)等服務(wù)項(xiàng)目。為回饋新老客戶創(chuàng)新互聯(lián)還提供了棗強(qiáng)免費(fèi)建站歡迎大家使用!
我的女朋友是個(gè)鐵憨憨,又菜又愛玩。
鐵憨憨:卡卡,最近好多同事都在聊React18,你給我講講唄?我要你用最通俗的語言把最底層的知識(shí)講明白,老娘的時(shí)間很寶貴的。
我:好啊,難得你要學(xué)習(xí),這是18所有新特性,你想先看哪個(gè)?
說著,我把屏幕轉(zhuǎn)向她。
鐵憨憨:“這個(gè)名字最長(zhǎng),一串英文一看就很厲害”
我一看,她指著Automatic batching(自動(dòng)批處理)
什么是批處理
鐵憨憨:“批處理,是不是和批發(fā)市場(chǎng)搞批發(fā)一個(gè)意思?”
雖然對(duì)這個(gè)比喻很無語,但不得不承認(rèn):還真挺像!
在React中,開發(fā)者通過調(diào)用this.setState(或useState的dispatch方法)觸發(fā)狀態(tài)更新。
狀態(tài)更新可能最終反映為視圖更新(取決于是否有DOM變化)。
開發(fā)者早已接受一個(gè)顯而易見的設(shè)定:「狀態(tài)」與「視圖」是一一對(duì)應(yīng)的。
但是,讓我們站在React團(tuán)隊(duì)的角度思考一個(gè)問題:
- 從this.setState調(diào)用到最終視圖更新,中間需要經(jīng)過源碼內(nèi)部的一系列工作。這一系列工作應(yīng)該是同步還是異步的呢?
如下例子中,a初始狀態(tài)為0,當(dāng)觸發(fā)onClick,調(diào)用兩次this.setState:
- // ...省略無關(guān)信息
- state = {
- a: 0
- }
- onClick() {
- this.setState({a: 1});
- console.log('a is:', this.state.a);
- this.setState({a: 2});
- }
- render() {
- const {a} = this.state;
- return
{a}
;- }
如果流程是異步的(即console.log打印a is:0),會(huì)有兩個(gè)潛在問題:
問題1:中間視圖狀態(tài)
當(dāng)狀態(tài)更新互相之間都是異步的,那么例子中頁(yè)面上的數(shù)字會(huì)從0先變?yōu)?,再變?yōu)?。
顯然更期望的行為是:數(shù)字直接從0變?yōu)?。
問題2:狀態(tài)更新的競(jìng)爭(zhēng)問題
{a: 1}與{a: 2}的狀態(tài)變化誰先反映到視圖更新?
畢竟在異步情況下,即使this.setState({a: 1})先觸發(fā),也可能this.setState({a: 2})的流程先完成。
開發(fā)者可不希望用戶點(diǎn)擊時(shí),有時(shí)候數(shù)字從0變?yōu)?,有時(shí)候變?yōu)?。
鐵憨憨:“好復(fù)雜啊,那就改為同步唄,能同時(shí)解決這兩個(gè)問題,還簡(jiǎn)單!”
確實(shí),如果狀態(tài)更新都是同步的,那么:
- 同步流程發(fā)生在同一個(gè)task(宏任務(wù)),不會(huì)出現(xiàn)視圖的中間狀態(tài)
- 更新之間有明確的順序,不會(huì)出現(xiàn)「競(jìng)爭(zhēng)問題」
但是,同步流程也意味著當(dāng)更新發(fā)生時(shí),瀏覽器會(huì)一直被JS線程阻塞(執(zhí)行更新流程)。
如果更新流程很復(fù)雜(應(yīng)用很大),或同時(shí)觸發(fā)很多更新,那么瀏覽器就會(huì)掉幀,表現(xiàn)為「瀏覽器卡頓」。
那該怎么辦呢?React團(tuán)隊(duì)給出的解決辦法就是:「批處理」(batchedUpdates)。
- 批處理:React會(huì)嘗試將同一上下文中觸發(fā)的更新合并為一個(gè)更新
在我們剛才的例子中:
- onClick() {
- this.setState({a: 1});
- console.log('a is:', this.state.a);
- this.setState({a: 2});
- }
兩次this.setState改變的狀態(tài)會(huì)按順序保存下來,最終只會(huì)觸發(fā)一次狀態(tài)更新。
這樣做的好處顯而易見:
- 合并不必要的更新,減少更新流程調(diào)用次數(shù)
- 狀態(tài)按順序保存下來,更新時(shí)不會(huì)出現(xiàn)「競(jìng)爭(zhēng)問題」
- 最終觸發(fā)的更新是異步流程,減少瀏覽器掉幀可能性
就像到批發(fā)市場(chǎng)拉貨。如果老板派幾輛小貨車去,可能由于路上耽擱,先去的車不一定先回(競(jìng)爭(zhēng)問題)。
還不如提前統(tǒng)計(jì)好要拉的貨,派一輛大貨車去,一次拉完了再回(批處理)。
鐵憨憨:“我明白了!不過為什么叫「自動(dòng)批處理」?難不成像槍一樣還有手動(dòng)、半自動(dòng)?”
是的,v18的「批處理」是自動(dòng)的。
v18之前的React使用半自動(dòng)「批處理」。
同時(shí),React提供了一個(gè)API——unstable_batchedupdates,這就是手動(dòng)「批處理」。
半自動(dòng)批處理
要聊「自動(dòng)批處理」,首先得聊「半自動(dòng)批處理」。
在v18之前,只有事件回調(diào)、生命周期回調(diào)中的更新會(huì)批處理,比如上例中的onClick。
而在promise、setTimeout等異步回調(diào)中不會(huì)批處理。
究其原因,讓我們看看批處理源碼(你不需要理解其中變量的意義,這不重要):
- export function batchedUpdates(fn: A => R, a: A): R {
- const prevExecutionContext = executionContext;
- executionContext |= BatchedContext;
- try {
- return fn(a);
- } finally {
- executionContext = prevExecutionContext;
- // If there were legacy sync updates, flush them at the end of the outer
- // most batchedUpdates-like method.
- if (executionContext === NoContext) {
- resetRenderTimer();
- flushSyncCallbacksOnlyInLegacyMode();
- }
- }
- }
可以看到,傳入一個(gè)回調(diào)函數(shù)fn,此時(shí)會(huì)通過「位運(yùn)算」為代表當(dāng)前執(zhí)行上下文狀態(tài)的變量executionContext增加BatchedContext狀態(tài)。
擁有這個(gè)狀態(tài)位代表當(dāng)前執(zhí)行上下文需要批處理。
在fn執(zhí)行過程中,其獲取到的全局變量executionContext都會(huì)包含BatchedContext。
最終fn執(zhí)行完后,進(jìn)入try...finally邏輯,將executionContext恢復(fù)為之前的上下文。
曾經(jīng)React源碼內(nèi)部,執(zhí)行onClick時(shí)的邏輯類似如下:
- batchedUpdates(onClick, e);
在onClick內(nèi)部的this.setState中,獲取到的executionContext包含BatchedContext,不會(huì)立刻進(jìn)入更新流程。
等退出該上下文后再統(tǒng)一執(zhí)行一次更新流程,這就是「半自動(dòng)批處理」。
鐵憨憨:“既然batchedUpdates是React自動(dòng)調(diào)用的,為啥是「半自動(dòng)批處理」?”
原因在于batchedUpdates方法是同步調(diào)用的。
如果fn有異步流程,比如如下例子:
- onClick() {
- setTimeout(() => {
- this.setState({a: 3});
- this.setState({a: 4});
- })
- }
那么在真正執(zhí)行this.setState時(shí)batchedUpdates早已執(zhí)行完,executionContext中已經(jīng)不包含BatchedContext。
此時(shí)觸發(fā)的更新不會(huì)走批處理邏輯。
所以這種「只對(duì)同步流程中的this.setState進(jìn)行批處理」,只能說是「半自動(dòng)」。
手動(dòng)批處理
為了彌補(bǔ)「半自動(dòng)批處理」的不靈活,ReactDOM中導(dǎo)出了unstable_batchedUpdates方法供開發(fā)者手動(dòng)調(diào)用。
比如如上例子,可以這樣修改:
- onClick() {
- setTimeout(() => {
- ReactDOM.unstable_batchedUpdates(() => {
- this.setState({a: 3});
- this.setState({a: 4});
- })
- })
- }
那么兩次this.setState調(diào)用時(shí)上下文中全局變量executionContext中會(huì)包含BatchedContext。
鐵憨憨:“你這么說我就理解批處理的實(shí)現(xiàn)了。不過v18是怎么實(shí)現(xiàn)在各種上下文環(huán)境都能批處理呢?有點(diǎn)神奇啊!”
自動(dòng)批處理
v18實(shí)現(xiàn)「自動(dòng)批處理」的關(guān)鍵在于兩點(diǎn):
- 增加調(diào)度的流程
- 不以全局變量executionContext為批處理依據(jù),而是以更新的「優(yōu)先級(jí)」為依據(jù)
鐵憨憨:“怎么冒出個(gè)「優(yōu)先級(jí)」?這是什么鬼?”
我:“那我先給你介紹介紹「更新」以及「優(yōu)先級(jí)」是什么意思吧。”
優(yōu)先級(jí)的意思
調(diào)用this.setState后源碼內(nèi)部會(huì)依次執(zhí)行:
- 根據(jù)當(dāng)前環(huán)境選擇一個(gè)「優(yōu)先級(jí)」
- 創(chuàng)造一個(gè)代表本次更新的update對(duì)象,賦予他步驟1的優(yōu)先級(jí)
- 將update掛載在當(dāng)前組件對(duì)應(yīng)fiber(虛擬DOM)上
- 進(jìn)入調(diào)度流程
以如下例子來說:
- onClick() {
- this.setState({a: 3});
- this.setState({a: 4});
- }
第一次執(zhí)行this.setState創(chuàng)造的update數(shù)據(jù)結(jié)構(gòu)如下:
第二次執(zhí)行this.setState創(chuàng)造的update數(shù)據(jù)結(jié)構(gòu)如下:
其中l(wèi)ane代表該update的優(yōu)先級(jí)。
在v18,不同場(chǎng)景下觸發(fā)的更新?lián)碛胁煌竷?yōu)先級(jí)」,比如:
- 如上例子中事件回調(diào)中的this.setState會(huì)產(chǎn)生同步優(yōu)先級(jí)的更新,這是最高的優(yōu)先級(jí)(lane為1)
為了對(duì)比,我們將如上代碼放入setTimeout中:
- onClick() {
- setTimeout(() => {
- this.setState({a: 3});
- this.setState({a: 4});
- })
- }
第一次執(zhí)行this.setState創(chuàng)造的update數(shù)據(jù)結(jié)構(gòu)如下:
第二次執(zhí)行this.setState創(chuàng)造的update數(shù)據(jù)結(jié)構(gòu)如下:
lane為16,代表Normal(即一般優(yōu)先級(jí))。
鐵憨憨:“所以每次調(diào)用this.setState會(huì)產(chǎn)生update對(duì)象,根據(jù)調(diào)用的場(chǎng)景他會(huì)擁有不同的lane(優(yōu)先級(jí)),是吧?”
我:“完全正確!”。
鐵憨憨:“那這和「批處理」有什么關(guān)系呢?”
我:“別急,這就是接下來進(jìn)入調(diào)度流程做的事了?!?/p>
調(diào)度流程
在組件對(duì)應(yīng)fiber掛載update后,就會(huì)進(jìn)入「調(diào)度流程」。
試想,一個(gè)大型應(yīng)用,在某一時(shí)刻,應(yīng)用的不同組件都觸發(fā)了更新。
那么在不同組件對(duì)應(yīng)的fiber中會(huì)存在不同優(yōu)先級(jí)的update。
「調(diào)度流程」的作用就是:選出這些update中優(yōu)先級(jí)最高的那個(gè),以該優(yōu)先級(jí)進(jìn)入更新流程。
讓我們節(jié)選部分「調(diào)度流程」的源碼:
- function ensureRootIsScheduled(root, currentTime) {
- // 獲取當(dāng)前所有優(yōu)先級(jí)中最高的優(yōu)先級(jí)
- var nextLanes = getNextLanes(root, root === workInProgressRoot ? workInProgressRootRenderLanes : NoLanes);
- // 本次要調(diào)度的優(yōu)先級(jí)
- var newCallbackPriority = getHighestPriorityLane(nextLanes);
- // 已經(jīng)存在的調(diào)度的優(yōu)先級(jí)
- var existingCallbackPriority = root.callbackPriority;
- if (existingCallbackPriority === newCallbackPriority) {
- return;
- }
- // 調(diào)度更新流程
- newCallbackNode = scheduleCallback(schedulerPriorityLevel, performConcurrentWorkOnRoot.bind(null, root));
- root.callbackPriority = newCallbackPriority;
- root.callbackNode = newCallbackNode;
- }
節(jié)選后的調(diào)度流程大體是:
- 獲取當(dāng)前所有優(yōu)先級(jí)中最高的優(yōu)先級(jí)
- 將步驟1的優(yōu)先級(jí)作為本次調(diào)度的優(yōu)先級(jí)
- 看是否已經(jīng)存在一個(gè)調(diào)度
- 如果已經(jīng)存在調(diào)度,且和當(dāng)前要調(diào)度的優(yōu)先級(jí)一致,則return
- 不一致的話就進(jìn)入調(diào)度流程
可以看到,調(diào)度的最終目的是在一定時(shí)間后執(zhí)行performConcurrentWorkOnRoot,正式進(jìn)入更新流程。
還是以上面的例子來說:
- onClick() {
- this.setState({a: 3});
- this.setState({a: 4});
- }
第一次調(diào)用this.setState,進(jìn)入「調(diào)度流程」后,不存在existingCallbackPriority。
所以會(huì)執(zhí)行調(diào)度:
- newCallbackNode = scheduleCallback(schedulerPriorityLevel, performConcurrentWorkOnRoot.bind(null, root));
第二次調(diào)用this.setState,進(jìn)入「調(diào)度流程」后,已經(jīng)存在existingCallbackPriority,即第一次調(diào)用產(chǎn)生的。
此時(shí)比較兩者優(yōu)先級(jí):
- if (existingCallbackPriority === newCallbackPriority) {
- return;
- }
由于兩個(gè)更新都是在onClick中觸發(fā),擁有同樣優(yōu)先級(jí),所以return。
按這個(gè)邏輯,即使多次調(diào)用this.setState,如:
- onClick() {
- this.setState({a: 3});
- this.setState({a: 4});
- this.setState({a: 5});
- this.setState({a: 6});
- }
只有第一次調(diào)用會(huì)執(zhí)行調(diào)度,后面幾次執(zhí)行由于優(yōu)先級(jí)和第一次一致會(huì)return。
當(dāng)一定時(shí)間過后,第一次調(diào)度的回調(diào)函數(shù)performConcurrentWorkOnRoot會(huì)執(zhí)行,進(jìn)入更新流程。
由于每次執(zhí)行this.setState都會(huì)創(chuàng)建update并掛載在fiber上。
所以即使只執(zhí)行一次更新流程,還是能將狀態(tài)更新到最新。
這就是以「優(yōu)先級(jí)」為依據(jù)的「自動(dòng)批處理」邏輯。
總結(jié)
通過本次講解,女朋友不僅學(xué)習(xí)了「批處理」的意義。還了解了「手動(dòng)/半自動(dòng)/自動(dòng)」三種形式的批處理。
最后我們還聊到了批處理的源碼實(shí)現(xiàn)邏輯。
分享文章:給女朋友講React18新特性:Automaticbatching
標(biāo)題鏈接:http://m.5511xx.com/article/ccspiod.html


咨詢
建站咨詢
