自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

給女朋友講React18新特性:Automatic batching

開發(fā) 前端
本篇主要講解了「批處理」的意義。還了解了「手動/半自動/自動」三種形式的批處理。最后我們還聊到了批處理的源碼實現(xiàn)邏輯。

[[406703]]

大家好,我是卡頌。

我的女朋友是個鐵憨憨,又菜又愛玩。

鐵憨憨:卡卡,最近好多同事都在聊React18,你給我講講唄?我要你用最通俗的語言把最底層的知識講明白,老娘的時間很寶貴的。

[[406704]]

我:好啊,難得你要學習,這是18所有新特性,你想先看哪個?

說著,我把屏幕轉向她。

鐵憨憨:“這個名字最長,一串英文一看就很厲害”

我一看,她指著Automatic batching(自動批處理)

什么是批處理

鐵憨憨:“批處理,是不是和批發(fā)市場搞批發(fā)一個意思?”

雖然對這個比喻很無語,但不得不承認:還真挺像!

在React中,開發(fā)者通過調(diào)用this.setState(或useState的dispatch方法)觸發(fā)狀態(tài)更新。

狀態(tài)更新可能最終反映為視圖更新(取決于是否有DOM變化)。

開發(fā)者早已接受一個顯而易見的設定:「狀態(tài)」與「視圖」是一一對應的。

但是,讓我們站在React團隊的角度思考一個問題:

  • 從this.setState調(diào)用到最終視圖更新,中間需要經(jīng)過源碼內(nèi)部的一系列工作。這一系列工作應該是同步還是異步的呢?

如下例子中,a初始狀態(tài)為0,當觸發(fā)onClick,調(diào)用兩次this.setState:

  1. // ...省略無關信息 
  2. state = { 
  3.   a: 0 
  4. onClick() { 
  5.   this.setState({a: 1}); 
  6.   console.log('a is:', this.state.a); 
  7.   this.setState({a: 2}); 
  8. render() { 
  9.   const {a} = this.state; 
  10.   return <p onClick={this.onClick}>{a}</p>; 

如果流程是異步的(即console.log打印a is:0),會有兩個潛在問題:

問題1:中間視圖狀態(tài)

當狀態(tài)更新互相之間都是異步的,那么例子中頁面上的數(shù)字會從0先變?yōu)?,再變?yōu)?。

顯然更期望的行為是:數(shù)字直接從0變?yōu)?。

問題2:狀態(tài)更新的競爭問題

{a: 1}與{a: 2}的狀態(tài)變化誰先反映到視圖更新?

畢竟在異步情況下,即使this.setState({a: 1})先觸發(fā),也可能this.setState({a: 2})的流程先完成。

開發(fā)者可不希望用戶點擊時,有時候數(shù)字從0變?yōu)?,有時候變?yōu)?。

鐵憨憨:“好復雜啊,那就改為同步唄,能同時解決這兩個問題,還簡單!”

確實,如果狀態(tài)更新都是同步的,那么:

  • 同步流程發(fā)生在同一個task(宏任務),不會出現(xiàn)視圖的中間狀態(tài)
  • 更新之間有明確的順序,不會出現(xiàn)「競爭問題」

但是,同步流程也意味著當更新發(fā)生時,瀏覽器會一直被JS線程阻塞(執(zhí)行更新流程)。

如果更新流程很復雜(應用很大),或同時觸發(fā)很多更新,那么瀏覽器就會掉幀,表現(xiàn)為「瀏覽器卡頓」。

那該怎么辦呢?React團隊給出的解決辦法就是:「批處理」(batchedUpdates)。

  • 批處理:React會嘗試將同一上下文中觸發(fā)的更新合并為一個更新

在我們剛才的例子中:

  1. onClick() { 
  2.   this.setState({a: 1}); 
  3.   console.log('a is:', this.state.a); 
  4.   this.setState({a: 2}); 

兩次this.setState改變的狀態(tài)會按順序保存下來,最終只會觸發(fā)一次狀態(tài)更新。

這樣做的好處顯而易見:

  • 合并不必要的更新,減少更新流程調(diào)用次數(shù)
  • 狀態(tài)按順序保存下來,更新時不會出現(xiàn)「競爭問題」
  • 最終觸發(fā)的更新是異步流程,減少瀏覽器掉幀可能性

就像到批發(fā)市場拉貨。如果老板派幾輛小貨車去,可能由于路上耽擱,先去的車不一定先回(競爭問題)。

還不如提前統(tǒng)計好要拉的貨,派一輛大貨車去,一次拉完了再回(批處理)。

鐵憨憨:“我明白了!不過為什么叫「自動批處理」?難不成像槍一樣還有手動、半自動?”

是的,v18的「批處理」是自動的。

v18之前的React使用半自動「批處理」。

同時,React提供了一個API——unstable_batchedupdates,這就是手動「批處理」。

半自動批處理

要聊「自動批處理」,首先得聊「半自動批處理」。

在v18之前,只有事件回調(diào)、生命周期回調(diào)中的更新會批處理,比如上例中的onClick。

而在promise、setTimeout等異步回調(diào)中不會批處理。

究其原因,讓我們看看批處理源碼(你不需要理解其中變量的意義,這不重要):

  1. export function batchedUpdates<A, R>(fn: A => R, a: A): R { 
  2.   const prevExecutionContext = executionContext; 
  3.   executionContext |= BatchedContext; 
  4.   try { 
  5.     return fn(a); 
  6.   } finally { 
  7.     executionContext = prevExecutionContext; 
  8.     // If there were legacy sync updates, flush them at the end of the outer 
  9.     // most batchedUpdates-like method. 
  10.     if (executionContext === NoContext) { 
  11.       resetRenderTimer(); 
  12.       flushSyncCallbacksOnlyInLegacyMode(); 
  13.     } 
  14.   } 

可以看到,傳入一個回調(diào)函數(shù)fn,此時會通過「位運算」為代表當前執(zhí)行上下文狀態(tài)的變量executionContext增加BatchedContext狀態(tài)。

擁有這個狀態(tài)位代表當前執(zhí)行上下文需要批處理。

在fn執(zhí)行過程中,其獲取到的全局變量executionContext都會包含BatchedContext。

最終fn執(zhí)行完后,進入try...finally邏輯,將executionContext恢復為之前的上下文。

曾經(jīng)React源碼內(nèi)部,執(zhí)行onClick時的邏輯類似如下:

  1. batchedUpdates(onClick, e); 

在onClick內(nèi)部的this.setState中,獲取到的executionContext包含BatchedContext,不會立刻進入更新流程。

等退出該上下文后再統(tǒng)一執(zhí)行一次更新流程,這就是「半自動批處理」。

鐵憨憨:“既然batchedUpdates是React自動調(diào)用的,為啥是「半自動批處理」?”

原因在于batchedUpdates方法是同步調(diào)用的。

如果fn有異步流程,比如如下例子:

  1. onClick() { 
  2.   setTimeout(() => { 
  3.     this.setState({a: 3}); 
  4.     this.setState({a: 4}); 
  5.   }) 

那么在真正執(zhí)行this.setState時batchedUpdates早已執(zhí)行完,executionContext中已經(jīng)不包含BatchedContext。

此時觸發(fā)的更新不會走批處理邏輯。

所以這種「只對同步流程中的this.setState進行批處理」,只能說是「半自動」。

手動批處理

為了彌補「半自動批處理」的不靈活,ReactDOM中導出了unstable_batchedUpdates方法供開發(fā)者手動調(diào)用。

比如如上例子,可以這樣修改:

  1. onClick() { 
  2.   setTimeout(() => { 
  3.     ReactDOM.unstable_batchedUpdates(() => { 
  4.       this.setState({a: 3}); 
  5.       this.setState({a: 4}); 
  6.     }) 
  7.   }) 

那么兩次this.setState調(diào)用時上下文中全局變量executionContext中會包含BatchedContext。

鐵憨憨:“你這么說我就理解批處理的實現(xiàn)了。不過v18是怎么實現(xiàn)在各種上下文環(huán)境都能批處理呢?有點神奇啊!”

[[406705]]

自動批處理

v18實現(xiàn)「自動批處理」的關鍵在于兩點:

  • 增加調(diào)度的流程
  • 不以全局變量executionContext為批處理依據(jù),而是以更新的「優(yōu)先級」為依據(jù)

鐵憨憨:“怎么冒出個「優(yōu)先級」?這是什么鬼?”

我:“那我先給你介紹介紹「更新」以及「優(yōu)先級」是什么意思吧。”

優(yōu)先級的意思

調(diào)用this.setState后源碼內(nèi)部會依次執(zhí)行:

  1. 根據(jù)當前環(huán)境選擇一個「優(yōu)先級」
  2. 創(chuàng)造一個代表本次更新的update對象,賦予他步驟1的優(yōu)先級
  3. 將update掛載在當前組件對應fiber(虛擬DOM)上
  4. 進入調(diào)度流程

以如下例子來說:

  1. onClick() { 
  2.   this.setState({a: 3}); 
  3.   this.setState({a: 4}); 

第一次執(zhí)行this.setState創(chuàng)造的update數(shù)據(jù)結構如下:

第二次執(zhí)行this.setState創(chuàng)造的update數(shù)據(jù)結構如下:

其中l(wèi)ane代表該update的優(yōu)先級。

在v18,不同場景下觸發(fā)的更新?lián)碛胁煌竷?yōu)先級」,比如:

  • 如上例子中事件回調(diào)中的this.setState會產(chǎn)生同步優(yōu)先級的更新,這是最高的優(yōu)先級(lane為1)

為了對比,我們將如上代碼放入setTimeout中:

  1. onClick() { 
  2.   setTimeout(() => { 
  3.     this.setState({a: 3}); 
  4.     this.setState({a: 4}); 
  5.   }) 

第一次執(zhí)行this.setState創(chuàng)造的update數(shù)據(jù)結構如下:

第二次執(zhí)行this.setState創(chuàng)造的update數(shù)據(jù)結構如下:

lane為16,代表Normal(即一般優(yōu)先級)。

鐵憨憨:“所以每次調(diào)用this.setState會產(chǎn)生update對象,根據(jù)調(diào)用的場景他會擁有不同的lane(優(yōu)先級),是吧?”

我:“完全正確!”。

鐵憨憨:“那這和「批處理」有什么關系呢?”

我:“別急,這就是接下來進入調(diào)度流程做的事了。”

調(diào)度流程

在組件對應fiber掛載update后,就會進入「調(diào)度流程」。

試想,一個大型應用,在某一時刻,應用的不同組件都觸發(fā)了更新。

那么在不同組件對應的fiber中會存在不同優(yōu)先級的update。

「調(diào)度流程」的作用就是:選出這些update中優(yōu)先級最高的那個,以該優(yōu)先級進入更新流程。

讓我們節(jié)選部分「調(diào)度流程」的源碼:

  1. function ensureRootIsScheduled(root, currentTime) { 
  2.  
  3.   // 獲取當前所有優(yōu)先級中最高的優(yōu)先級 
  4.   var nextLanes = getNextLanes(root, root === workInProgressRoot ? workInProgressRootRenderLanes : NoLanes); 
  5.   // 本次要調(diào)度的優(yōu)先級 
  6.   var newCallbackPriority = getHighestPriorityLane(nextLanes);  
  7.    
  8.   // 已經(jīng)存在的調(diào)度的優(yōu)先級 
  9.   var existingCallbackPriority = root.callbackPriority; 
  10.  
  11.   if (existingCallbackPriority === newCallbackPriority) { 
  12.     return
  13.   } 
  14.   // 調(diào)度更新流程 
  15.   newCallbackNode = scheduleCallback(schedulerPriorityLevel, performConcurrentWorkOnRoot.bind(null, root)); 
  16.  
  17.   root.callbackPriority = newCallbackPriority; 
  18.   root.callbackNode = newCallbackNode; 

節(jié)選后的調(diào)度流程大體是:

  1. 獲取當前所有優(yōu)先級中最高的優(yōu)先級
  2. 將步驟1的優(yōu)先級作為本次調(diào)度的優(yōu)先級
  3. 看是否已經(jīng)存在一個調(diào)度
  4. 如果已經(jīng)存在調(diào)度,且和當前要調(diào)度的優(yōu)先級一致,則return
  5. 不一致的話就進入調(diào)度流程

可以看到,調(diào)度的最終目的是在一定時間后執(zhí)行performConcurrentWorkOnRoot,正式進入更新流程。

還是以上面的例子來說:

  1. onClick() { 
  2.   this.setState({a: 3}); 
  3.   this.setState({a: 4}); 

第一次調(diào)用this.setState,進入「調(diào)度流程」后,不存在existingCallbackPriority。

所以會執(zhí)行調(diào)度:

  1. newCallbackNode = scheduleCallback(schedulerPriorityLevel, performConcurrentWorkOnRoot.bind(null, root)); 

第二次調(diào)用this.setState,進入「調(diào)度流程」后,已經(jīng)存在existingCallbackPriority,即第一次調(diào)用產(chǎn)生的。

此時比較兩者優(yōu)先級:

  1. if (existingCallbackPriority === newCallbackPriority) { 
  2.   return

由于兩個更新都是在onClick中觸發(fā),擁有同樣優(yōu)先級,所以return。

按這個邏輯,即使多次調(diào)用this.setState,如:

  1. onClick() { 
  2.   this.setState({a: 3}); 
  3.   this.setState({a: 4}); 
  4.   this.setState({a: 5}); 
  5.   this.setState({a: 6}); 

只有第一次調(diào)用會執(zhí)行調(diào)度,后面幾次執(zhí)行由于優(yōu)先級和第一次一致會return。

當一定時間過后,第一次調(diào)度的回調(diào)函數(shù)performConcurrentWorkOnRoot會執(zhí)行,進入更新流程。

由于每次執(zhí)行this.setState都會創(chuàng)建update并掛載在fiber上。

所以即使只執(zhí)行一次更新流程,還是能將狀態(tài)更新到最新。

這就是以「優(yōu)先級」為依據(jù)的「自動批處理」邏輯。

總結

通過本次講解,女朋友不僅學習了「批處理」的意義。還了解了「手動/半自動/自動」三種形式的批處理。

最后我們還聊到了批處理的源碼實現(xiàn)邏輯。

 

責任編輯:姜華 來源: 魔術師卡頌
相關推薦

2021-06-22 07:45:57

React18startTransiReact

2021-11-01 19:49:55

React組件模式

2021-06-16 06:05:25

React18React

2020-10-25 08:22:28

V8 引擎JavaScript回調(diào)函數(shù)

2021-11-29 06:05:31

React組件前端

2019-03-12 09:43:14

反向代理正向代理服務器

2021-10-21 08:31:31

Spring循環(huán)依賴面試

2021-03-05 17:06:53

全排列組合子集

2019-04-09 09:40:23

2020-03-16 14:08:59

線程熔斷限流

2022-03-16 17:01:35

React18并發(fā)的React組件render

2022-03-30 14:22:55

ReactReact18并發(fā)特性

2023-03-21 08:31:13

ReconcilerFiber架構

2021-09-14 12:00:11

VR字節(jié)跳動

2019-10-09 10:45:16

云計算Web互聯(lián)網(wǎng)

2022-04-27 07:37:42

ReactReact18

2020-10-19 13:01:31

刪庫程序員思科

2021-03-11 16:45:29

TCP程序C語言

2019-04-19 09:48:53

樂觀鎖悲觀鎖數(shù)據(jù)庫

2021-05-19 11:02:44

PythonTurtle參數(shù)
點贊
收藏

51CTO技術棧公眾號