React狀態(tài)更新的優(yōu)先級(jí)機(jī)制源碼解析
為什么需要優(yōu)先級(jí)
優(yōu)先級(jí)機(jī)制最終目的是為了實(shí)現(xiàn)高優(yōu)先級(jí)任務(wù)優(yōu)先執(zhí)行,低優(yōu)先級(jí)任務(wù)延后執(zhí)行。
實(shí)現(xiàn)這一目的的本質(zhì)就是在低優(yōu)先級(jí)任務(wù)執(zhí)行時(shí),有更高優(yōu)先級(jí)任務(wù)進(jìn)來(lái)的話,可以打斷低優(yōu)先級(jí)任務(wù)的執(zhí)行。
同步模式下的react運(yùn)行時(shí)
我們知道在同步模式下,從 setState
到 虛擬DOM遍歷,再到真實(shí)DOM更新,整個(gè)過(guò)程都是同步執(zhí)行且無(wú)法被中斷的,這樣可能就會(huì)出現(xiàn)一個(gè)問(wèn)題 —— 用戶事件觸發(fā)的更新被阻塞。
什么是用戶事件觸發(fā)的更新被阻塞?如果 React
正在進(jìn)行更新任務(wù),此時(shí)用戶觸發(fā)了交互事件,且在事件回調(diào)中執(zhí)行了 setState
,在同步模式下,這個(gè)更新任務(wù)需要 等待 當(dāng)前正在更新的任務(wù)完成之后,才會(huì)被執(zhí)行。假如當(dāng)前 React
正在進(jìn)行的更新任務(wù)耗時(shí)比較久,用戶事件觸發(fā)的更新任務(wù)不能及時(shí)被執(zhí)行,造成下個(gè)更新任務(wù)被阻塞,從而形成了卡頓。
這時(shí)候,我們就希望能夠及時(shí)響應(yīng)用戶觸發(fā)的事件,優(yōu)先執(zhí)行用戶事件觸發(fā)的更新任務(wù),也就是我們說(shuō)的異步模式
我們可以比較一下,同步模式下和異步模式(優(yōu)先級(jí)機(jī)制)下更新任務(wù)執(zhí)行的差異
import React from "react"; import "./styles.css"; export default class extends React.Component { constructor() { super(); this.state = { list: new Array(10000).fill(1), }; this.domRef = null; } componentDidMount() { setTimeout(() => { console.log("setTimeout 準(zhǔn)備更新", performance.now()); this.setState( { list: new Array(10000).fill(Math.random() * 10000), updateLanes: 16 }, () => { console.log("setTimeout 更新完畢", performance.now()); } ); }, 100); setTimeout(() => { this.domRef.click(); }, 150); } render() { const { list } = this.state; return ( <div ref={(v) => (this.domRef = v)} className="App" onClick={() => { console.log("click 準(zhǔn)備更新", performance.now()); this.setState( { list: new Array(10000).fill(2), updateLanes: 1 }, () => { console.log("click 更新完畢", performance.now()); } ); }} > {list.map((i, index) => ( <h2 key={i + +index}>Hello {i}</h2> ))} </div> ); } }
click事件
觸發(fā)的更新,會(huì)比 setTimeout
觸發(fā)的更新更優(yōu)先執(zhí)行,做到了及時(shí)響應(yīng)用戶事件,打斷 setTimeout
更新任務(wù)(低優(yōu)先級(jí)任務(wù))的執(zhí)行。
如何運(yùn)用優(yōu)先級(jí)機(jī)制優(yōu)化react運(yùn)行時(shí)
為了解決同步模式渲染下的缺陷,我們希望能夠?qū)?react
做出下面這些優(yōu)化
- 確定不同場(chǎng)景下所觸發(fā)更新的優(yōu)先級(jí),以便我們可以決定優(yōu)先執(zhí)行哪些任務(wù)
- 若有更高優(yōu)先級(jí)的任務(wù)進(jìn)來(lái),我們需要打斷當(dāng)前進(jìn)行的任務(wù),然后執(zhí)行這個(gè)高優(yōu)先級(jí)任務(wù)
- 確保低優(yōu)先級(jí)任務(wù)不會(huì)被一直打斷,在一定時(shí)間后能夠被升級(jí)為最高優(yōu)先級(jí)的任務(wù)
確定不同場(chǎng)景下的調(diào)度優(yōu)先級(jí)
看過(guò) react
源碼的小伙伴可能都會(huì)有一個(gè)疑惑,為什么源碼里面有那么多優(yōu)先級(jí)相關(guān)的單詞??怎么區(qū)分他們呢?
其實(shí)在 react
中主要分為兩類優(yōu)先級(jí),scheduler
優(yōu)先級(jí)和 lane
優(yōu)先級(jí),lane
優(yōu)先級(jí)下面又派生出 event
優(yōu)先級(jí)
- lane 優(yōu)先級(jí):主要用于任務(wù)調(diào)度前,對(duì)當(dāng)前正在進(jìn)行的任務(wù)和被調(diào)度任務(wù)做一個(gè)優(yōu)先級(jí)校驗(yàn),判斷是否需要打斷當(dāng)前正在進(jìn)行的任務(wù)
- event 優(yōu)先級(jí):本質(zhì)上也是lane優(yōu)先級(jí),lane優(yōu)先級(jí)是通用的,event優(yōu)先級(jí)更多是結(jié)合瀏覽器原生事件,對(duì)lane優(yōu)先級(jí)做了分類和映射
- scheduler 優(yōu)先級(jí):主要用在時(shí)間分片中任務(wù)過(guò)期時(shí)間的計(jì)算
lane優(yōu)先級(jí)
可以用賽道的概念去理解lane優(yōu)先級(jí),lane優(yōu)先級(jí)有31個(gè),我們可以用31位的二進(jìn)制值去表示,值的每一位代表一條賽道對(duì)應(yīng)一個(gè)lane優(yōu)先級(jí),賽道位置越靠前,優(yōu)先級(jí)越高
優(yōu)先級(jí) | 十進(jìn)制值 | 二進(jìn)制值 | 賽道位置 |
---|---|---|---|
NoLane | 0 | 0000000000000000000000000000000 | 0 |
SyncLane | 1 | 0000000000000000000000000000001 | 0 |
InputContinuousHydrationLane | 2 | 0000000000000000000000000000010 | 1 |
InputContinuousLane | 4 | 0000000000000000000000000000100 | 2 |
DefaultHydrationLane | 8 | 0000000000000000000000000001000 | 3 |
DefaultLane | 16 | 0000000000000000000000000010000 | 4 |
TransitionHydrationLane | 32 | 0000000000000000000000000100000 | 5 |
TransitionLane1 | 64 | 0000000000000000000000001000000 | 6 |
TransitionLane2 | 128 | 0000000000000000000000010000000 | 7 |
TransitionLane3 | 256 | 0000000000000000000000100000000 | 8 |
TransitionLane4 | 512 | 0000000000000000000001000000000 | 9 |
TransitionLane5 | 1024 | 0000000000000000000010000000000 | 10 |
TransitionLane | 2048 | 0000000000000000000100000000000 | 11 |
TransitionLane7 | 4096 | 0000000000000000001000000000000 | 12 |
TransitionLane8 | 8192 | 0000000000000000010000000000000 | 13 |
TransitionLane9 | 16384 | 0000000000000000100000000000000 | 14 |
TransitionLane10 | 32768 | 0000000000000001000000000000000 | 15 |
TransitionLane11 | 65536 | 0000000000000010000000000000000 | 16 |
TransitionLane12 | 131072 | 0000000000000100000000000000000 | 17 |
TransitionLane13 | 262144 | 0000000000001000000000000000000 | 18 |
TransitionLane14 | 524288 | 0000000000010000000000000000000 | 19 |
TransitionLane15 | 1048576 | 0000000000100000000000000000000 | 20 |
TransitionLane16 | 2097152 | 0000000001000000000000000000000 | 21 |
RetryLane1 | 4194304 | 0000000010000000000000000000000 | 22 |
RetryLane2 | 8388608 | 0000000100000000000000000000000 | 23 |
RetryLane3 | 16777216 | 0000001000000000000000000000000 | 24 |
RetryLane4 | 33554432 | 0000010000000000000000000000000 | 25 |
RetryLane5 | 67108864 | 0000100000000000000000000000000 | 26 |
SelectiveHydrationLane | 134217728 | 0001000000000000000000000000000 | 27 |
IdleHydrationLane | 268435456 | 0010000000000000000000000000000 | 28 |
IdleLane | 536870912 | 0100000000000000000000000000000 | 29 |
OffscreenLane | 1073741824 | 1000000000000000000000000000000 | 30 |
event優(yōu)先級(jí)
EventPriority | Lane | 數(shù)值 | |
---|---|---|---|
DiscreteEventPriority | 離散事件。click、keydown、focusin等,事件的觸發(fā)不是連續(xù),可以做到快速響應(yīng) | SyncLane | 1 |
ContinuousEventPriority | 連續(xù)事件。drag、scroll、mouseover等,事件的是連續(xù)觸發(fā)的,快速響應(yīng)可能會(huì)阻塞渲染,優(yōu)先級(jí)較離散事件低 | InputContinuousLane | 4 |
DefaultEventPriority | 默認(rèn)的事件優(yōu)先級(jí) | DefaultLane | 16 |
IdleEventPriority | 空閑的優(yōu)先級(jí) | IdleLane | 536870912 |
scheduler優(yōu)先級(jí)
SchedulerPriority | EventPriority | 大于>17.0.2 | 小于>17.0.2 |
---|---|---|---|
ImmediatePriority | DiscreteEventPriority | 1 | 99 |
UserblockingPriority | Userblocking | 2 | 98 |
NormalPriority | DefaultEventPriority | 3 | 97 |
LowPriority | DefaultEventPriority | 4 | 96 |
IdlePriority | IdleEventPriority | 5 | 95 |
NoPriority | 0 | 90 |
優(yōu)先級(jí)間的轉(zhuǎn)換
lane優(yōu)先級(jí) 轉(zhuǎn) event優(yōu)先級(jí)(參考 lanesToEventPriority
函數(shù))
- 轉(zhuǎn)換規(guī)則:以區(qū)間的形式根據(jù)傳入的lane返回對(duì)應(yīng)的 event 優(yōu)先級(jí)。比如傳入的優(yōu)先級(jí)不大于 Discrete 優(yōu)先級(jí),就返回 Discrete 優(yōu)先級(jí),以此類推
event優(yōu)先級(jí) 轉(zhuǎn) scheduler優(yōu)先級(jí)(參考 ensureRootIsScheduled
函數(shù))
- 轉(zhuǎn)換規(guī)則:可以參考上面scheduler優(yōu)先級(jí)表
event優(yōu)先級(jí) 轉(zhuǎn) lane優(yōu)先級(jí)(參考 getEventPriority
函數(shù))
- 轉(zhuǎn)換規(guī)則:對(duì)于非離散、連續(xù)的事件,會(huì)根據(jù)一定規(guī)則作轉(zhuǎn)換,具體課參考上面 event 優(yōu)先級(jí)表,
優(yōu)先級(jí)機(jī)制如何設(shè)計(jì)
說(shuō)到優(yōu)先級(jí)機(jī)制,我們可能馬上能聯(lián)想到的是優(yōu)先級(jí)隊(duì)列,其最突出的特性是最高優(yōu)先級(jí)先出,react
的優(yōu)先級(jí)機(jī)制跟優(yōu)先級(jí)隊(duì)列類似,不過(guò)其利用了賽道的概念,配合位與運(yùn)算豐富了隊(duì)列的功能,比起優(yōu)先級(jí)隊(duì)列,讀寫(xiě)速度更快,更加容易理解
設(shè)計(jì)思路
- 合并賽道:維護(hù)一個(gè)隊(duì)列,可以存儲(chǔ)被占用的賽道
- 釋放賽道:根據(jù)優(yōu)先級(jí)釋放對(duì)應(yīng)被占用賽道
- 找出最高優(yōu)先級(jí)賽道:獲取隊(duì)列中最高優(yōu)先級(jí)賽道
- 快速定位賽道索引:根據(jù)優(yōu)先級(jí)獲取賽道在隊(duì)列中所在的位置
- 判斷賽道是否被占用:根據(jù)傳入優(yōu)先級(jí)判斷該優(yōu)先級(jí)所在賽道是否被占用
合并賽道
場(chǎng)景
- 比如當(dāng)前正在調(diào)度的任務(wù)優(yōu)先級(jí)是DefaultLane,用戶點(diǎn)擊觸發(fā)更新,有一個(gè)高優(yōu)先級(jí)的任務(wù)SyncLane產(chǎn)生,需要存儲(chǔ)這個(gè)任務(wù)所占用的賽道
運(yùn)算過(guò)程
- 運(yùn)算方式:位或運(yùn)算 -
a | b
- 運(yùn)算結(jié)果:DefaultLane和SyncLane分別占用了第1條和第5條賽道
DefaultLane優(yōu)先級(jí)為16,SyncLane優(yōu)先級(jí)為1
16 | 1 = 17
17的二進(jìn)制值為10001
16的二進(jìn)制值為10000,1的二進(jìn)制值為00001
釋放賽道
場(chǎng)景
- SyncLane 任務(wù)執(zhí)行完,需要釋放占用的賽道
運(yùn)算過(guò)程
- 運(yùn)算方式:位與+位非 -
a & ~b
- 運(yùn)算結(jié)果:SyncLane賽道被釋放,只剩下DefaultLane賽道
17 & ~1 = 16
17的二進(jìn)制值為10001為什么用位非?
~1 = -2
2 的二進(jìn)制是00010,-2的話符號(hào)位取反變?yōu)?0010
10001和10010進(jìn)行位與運(yùn)算得到10000,也就是十進(jìn)制的16
找出最高優(yōu)先級(jí)賽道
場(chǎng)景
- 當(dāng)前有 DefaultLane 和 SyncLane 兩個(gè)優(yōu)先級(jí)的任務(wù)占用賽道,在進(jìn)入 ensureRootIsScheduled 方法后,我需要先調(diào)度優(yōu)先級(jí)最高的任務(wù),所以需要找出當(dāng)前優(yōu)先級(jí)最高的賽道
運(yùn)算過(guò)程
- 運(yùn)算方式:位與+符號(hào)位取反 -
a & -b
- 運(yùn)算結(jié)果:找到了最高優(yōu)先級(jí)的任務(wù)SyncLane,SyncLane任務(wù)為同步任務(wù),Scheduler將以同步優(yōu)先級(jí)調(diào)度當(dāng)前應(yīng)用根節(jié)點(diǎn)
17 & -17 = 1
17的二進(jìn)制值為10001
-17的二進(jìn)制值為00001
10001和00001進(jìn)行位與運(yùn)算得到1,也就是SyncLane
快速定位賽道索引
場(chǎng)景
- 饑餓任務(wù)喚醒:在發(fā)起調(diào)度前,我們需要對(duì)隊(duì)列中的所有賽道進(jìn)行一個(gè)判斷,判斷該賽道的任務(wù)是否過(guò)期,如果過(guò)期,就優(yōu)先執(zhí)行該過(guò)期任務(wù)。為此,需要維護(hù)一個(gè)長(zhǎng)度為31的數(shù)組,數(shù)組的每個(gè)元素的下標(biāo)索引與31個(gè)優(yōu)先級(jí)賽道一一對(duì)應(yīng),數(shù)組中存儲(chǔ)的是任務(wù)的過(guò)期時(shí)間,在判斷時(shí),我們希望能根據(jù)優(yōu)先級(jí)快速找到該優(yōu)先級(jí)在數(shù)組中對(duì)應(yīng)的位置。
運(yùn)算過(guò)程
- 運(yùn)算方式:Math.clz32
- 運(yùn)算結(jié)果:找到了DefaultLane的索引位置為4,那就可以釋放應(yīng)用根節(jié)點(diǎn)上的eventTimes、expirationTimes,將其所在位置的值賦值為-1,然后執(zhí)行對(duì)應(yīng)的過(guò)期任務(wù)
// 找出 DefaultLane 賽道索引
31 - Math.clz32(16) = 416的二進(jìn)制值為10000
索引4對(duì)應(yīng)的就是第五個(gè)賽道
Math.clz32是用來(lái)干什么的?
- 獲取一個(gè)十進(jìn)制數(shù)字對(duì)應(yīng)二進(jìn)制值中開(kāi)頭0的個(gè)數(shù)。
- 所以用31減去
Math.clz32
的值就能得到該賽道的索引
判斷賽道是否被占用
異步模式下會(huì)存在高優(yōu)先級(jí)任務(wù)插隊(duì)的情況,此情況下 state
的計(jì)算方式會(huì)跟同步模式下**有些不同。
場(chǎng)景
我們 setState 之后并不是馬上就會(huì)更新 state
,而是會(huì)根據(jù) setState 的內(nèi)容生成一個(gè) Update
對(duì)象,這個(gè)對(duì)象包含了更新內(nèi)容、更新優(yōu)先級(jí)等屬性。
更新 state
這個(gè)動(dòng)作是在 processUpdateQueue
函數(shù)里進(jìn)行的,函數(shù)里面會(huì)判斷 Update
對(duì)象的優(yōu)先級(jí)所在賽道是否被占用,來(lái)決定是否在此輪任務(wù)中計(jì)算這個(gè) Update
對(duì)象的 state
- 如果被占用,代表
Update
對(duì)象優(yōu)先級(jí)和當(dāng)前正在進(jìn)行的任務(wù)相等,可以根據(jù)Update
對(duì)象計(jì)算state
并更新到 Fiber 節(jié)點(diǎn)的memoizedState
屬性上 - 如果未被占用,代表當(dāng)前正在進(jìn)行的任務(wù)優(yōu)先級(jí)比這個(gè)
Update
對(duì)象優(yōu)先級(jí)高,相應(yīng)的這個(gè)低優(yōu)先級(jí)的Update
對(duì)象將暫不被計(jì)算state,留到下一輪低優(yōu)先級(jí)任務(wù)被重啟時(shí)再進(jìn)行計(jì)算
運(yùn)算過(guò)程
- 運(yùn)算方式:位與
(renderLanes & updateLanes) == updateLanes
- 運(yùn)算結(jié)果:0代表當(dāng)前調(diào)度優(yōu)先級(jí)高于某個(gè)Update對(duì)象優(yōu)先級(jí)
運(yùn)算公式
(1 & 16) == 161的二進(jìn)制值為00001
16的二進(jìn)制值為10000
00001和10000進(jìn)行位與運(yùn)算得到0
如何將優(yōu)先級(jí)機(jī)制融入React運(yùn)行時(shí)
生成一個(gè)更新任務(wù)
生成任務(wù)的流程其實(shí)非常簡(jiǎn)單,入口就在我們常用的 setState
函數(shù),先上圖
setState
函數(shù)內(nèi)部執(zhí)行的就是 enqueueUpdate
函數(shù),而 enqueueUpdate
函數(shù)的工作主要分為4步:
- 獲取本次更新的優(yōu)先級(jí)。
- 創(chuàng)建
Update
對(duì)象 - 將本次更新優(yōu)先級(jí)關(guān)聯(lián)到當(dāng)前Fiber節(jié)點(diǎn)、父級(jí)節(jié)點(diǎn)和應(yīng)用根節(jié)點(diǎn)
- 發(fā)起
ensureRootIsScheduled
調(diào)度。
步驟一:獲取本次更新的優(yōu)先級(jí)
步驟一的工作是調(diào)用 requestUpdateLane
函數(shù)拿到此次更新任務(wù)的優(yōu)先級(jí)
如果當(dāng)前為非 concurrent
模式
- 當(dāng)前不在 render 階段。返回 syncLane
- 當(dāng)前正在 render 階段。返回 workInProgressRootRenderLanes 中最高的優(yōu)先級(jí)(這里就用到上面的優(yōu)先級(jí)運(yùn)算機(jī)制,找出最高優(yōu)先級(jí)賽道)
如果當(dāng)前為 concurrent
模式
- 需要執(zhí)行延遲任務(wù)的話,比如
Suspend
、useTransition
、useDefferedValue
等特性。在transition
類型的優(yōu)先級(jí)中尋找空閑的賽道。transition
類型的賽道有 16 條,從第 1 條到第 16 條,當(dāng)?shù)竭_(dá)第 16 條賽道后,下一次transition
類型的任務(wù)會(huì)回到第 1 條賽道,如此往復(fù)。 - 執(zhí)行
getCurrentUpdatePriority
函數(shù)。獲取當(dāng)前更新優(yōu)先級(jí)。如果不為NoLane
就返回 - 執(zhí)行
getCurrentEventPriority
函數(shù)。返回當(dāng)前的事件優(yōu)先級(jí)。如果沒(méi)有事件產(chǎn)生,返回DefaultEventPriority
總的來(lái)說(shuō),requestUpdateLane
函數(shù)的優(yōu)先級(jí)選取判斷順序如下:
SyncLane >> TransitionLane >> UpdateLane >> EventLane
估計(jì)有很多小伙伴都會(huì)很困惑一個(gè)問(wèn)題,為什么會(huì)有這么多獲取優(yōu)先級(jí)的函數(shù),這里我整理了一下其他函數(shù)的職責(zé)
步驟二:創(chuàng)建 Update 對(duì)象
這里的代碼量不多,其實(shí)就是將 setState 的參數(shù)用一個(gè)對(duì)象封裝起來(lái),留給 render 階段用
function createUpdate(eventTime, lane) { var update = { eventTime: eventTime, lane: lane, tag: UpdateState, payload: null, callback: null, next: null }; return update; }
步驟三:關(guān)聯(lián)優(yōu)先級(jí)
在這里先解釋兩個(gè)概念,一個(gè)是 HostRoot
,一個(gè)是 FiberRootNode
HostRoot
:就是ReactDOM.render
的第一個(gè)參數(shù),組件樹(shù)的根節(jié)點(diǎn)。HostRoot
可能會(huì)存在多個(gè),因?yàn)?ReactDOM.render
可以多次調(diào)用FiberRootNode
:react 的應(yīng)用根節(jié)點(diǎn),每個(gè)頁(yè)面只有一個(gè) react 的應(yīng)用根節(jié)點(diǎn)??梢詮?HostRoot
節(jié)點(diǎn)的stateNode
屬性訪問(wèn)
這里關(guān)聯(lián)優(yōu)先級(jí)主要執(zhí)行了兩個(gè)函數(shù)
markUpdateLaneFromFiberToRoot
。該函數(shù)主要做了兩個(gè)事情
- 將優(yōu)先級(jí)合并到當(dāng)前 Fiber 節(jié)點(diǎn)的 lanes 屬性中
- 將優(yōu)先級(jí)合并到父級(jí)節(jié)點(diǎn)的 childLanes 屬性中(告訴父節(jié)點(diǎn)他的子節(jié)點(diǎn)有多少條賽道要跑) 但因?yàn)楹瘮?shù)傳入的 Fiber 節(jié)點(diǎn)是
HostRoot
,也就是ReactDOM.render
的根節(jié)點(diǎn),也就是說(shuō)沒(méi)有父節(jié)點(diǎn)了,所以第二件事情沒(méi)有做
markRootUpdated
。該函數(shù)也是主要做了兩個(gè)事情
- 將待調(diào)度任務(wù)優(yōu)先級(jí)合并到當(dāng)前 react 應(yīng)用根節(jié)點(diǎn)上
- 計(jì)算當(dāng)前任務(wù)優(yōu)先級(jí)賽道占用的開(kāi)始時(shí)間(eventTime)
由此可見(jiàn),react
的優(yōu)先級(jí)機(jī)制并不獨(dú)立運(yùn)行在每一個(gè)組件節(jié)點(diǎn)里面,而是依賴一個(gè)全局的 react
應(yīng)用根節(jié)點(diǎn)去控制下面多個(gè)組件樹(shù)的任務(wù)調(diào)度
將優(yōu)先級(jí)關(guān)聯(lián)到這些Fiber節(jié)點(diǎn)有什么用?
先說(shuō)說(shuō)他們的區(qū)別
- lanes:只存在非 react 應(yīng)用根節(jié)點(diǎn)上,記錄當(dāng)前 Fiber 節(jié)點(diǎn)的 lane 優(yōu)先級(jí)
- childLanes:只存在非 react 應(yīng)用根節(jié)點(diǎn)上,記錄當(dāng)前 Fiber 節(jié)點(diǎn)下的所有子 Fiber 節(jié)點(diǎn)的 lane 優(yōu)先級(jí)
- pendingLanes:只存在 react 應(yīng)用根節(jié)點(diǎn)上,記錄的是所有
HostRoot
的 lane 優(yōu)先級(jí)
具體應(yīng)用場(chǎng)景
- 釋放賽道。上面說(shuō)的優(yōu)先級(jí)運(yùn)算機(jī)制提到了任務(wù)執(zhí)行完畢會(huì)釋放賽道,具體來(lái)說(shuō)是在 commit 階段結(jié)束之后釋放被占用的優(yōu)先級(jí),也就是
markRootFinished
函數(shù)。 - 判斷賽道是否被占用。在 render 階段的
beginWork
流程里面,會(huì)有很多判斷childLanes
是否被占用的判斷
步驟四:發(fā)起調(diào)度
調(diào)度里面最關(guān)鍵的一步,就是 ensureRootIsScheduled
函數(shù)的調(diào)用,該函數(shù)的邏輯就是由下面兩大部分構(gòu)成,高優(yōu)先級(jí)任務(wù)打斷低優(yōu)先級(jí)任務(wù) 和 饑餓任務(wù)問(wèn)題
高優(yōu)先級(jí)任務(wù)打斷低優(yōu)先級(jí)任務(wù)
該部分流程可以分為三部曲
- cancelCallback
- pop(taskQueue)
- 低優(yōu)先級(jí)任務(wù)重啟
cancelCallback
var existingCallbackNode = root.callbackNode; var existingCallbackPriority = root.callbackPriority; var newCallbackPriority = getHighestPriorityLane(nextLanes); if (existingCallbackPriority === newCallbackPriority) { ... return; } if (existingCallbackNode != null) { cancelCallback(existingCallbackNode); } newCallbackNode = scheduleCallback( schedulerPriorityLevel, performConcurrentWorkOnRoot.bind(null, root) ); root.callbackPriority = newCallbackPriority; root.callbackNode = newCallbackNode;
上面是 ensureRootIsScheduled
函數(shù)的一些代碼片段,先對(duì)變量做解釋
existingCallbackNode
:當(dāng)前 render 階段正在進(jìn)行的任務(wù)
existingCallbackPriority
:當(dāng)前 render 階段正在進(jìn)行的任務(wù)優(yōu)先級(jí)
newCallbackPriority
:此次調(diào)度優(yōu)先級(jí)
這里會(huì)判斷 existingCallbackPriority
和 newCallbackPriority
兩個(gè)優(yōu)先級(jí)是否相等,如果相等,此次更新合并到當(dāng)前正在進(jìn)行的任務(wù)中。如果不相等,代表此次更新任務(wù)的優(yōu)先級(jí)更高,需要打斷當(dāng)前正在進(jìn)行的任務(wù)
如何打斷任務(wù)?
- 關(guān)鍵函數(shù)
cancelCallback(existingCallbackNode)
,cancelCallback
函數(shù)就是將root.callbackNode
賦值為null performConcurrentWorkOnRoot
函數(shù)會(huì)先把root.callbackNode
緩存起來(lái),在函數(shù)末尾會(huì)再判斷root.callbackNode
和開(kāi)始緩存起來(lái)的值是否一樣,如果不一樣,就代表root.callbackNode
被賦值為null了,有更高優(yōu)先級(jí)任務(wù)進(jìn)來(lái)。- 此時(shí)
performConcurrentWorkOnRoot
返回值為null
下面是 performConcurrentWorkOnRoot
代碼片段
... var originalCallbackNode = root.callbackNode; ... // 函數(shù)末尾 if (root.callbackNode === originalCallbackNode) { return performConcurrentWorkOnRoot.bind(null, root); } return null;
由上面 ensureRootIsScheduled
的代碼片段可以知道,performConcurrentWorkOnRoot
函數(shù)是被 scheduleCallback
函數(shù)調(diào)度的,具體返回后的邏輯需要到 Scheduler
模塊去找
pop(taskQueue)
var callback = currentTask.callback; if (typeof callback === 'function') { ... } else { pop(taskQueue); }
上面是 Scheduler
模塊里面 workLoop
函數(shù)的代碼片段,currentTask.callback
就是 scheduleCallback
的第二個(gè)參數(shù),也就是performConcurrentWorkOnRoot
函數(shù)
承接上個(gè)主題,如果 performConcurrentWorkOnRoot
函數(shù)返回了null,workLoop
內(nèi)部就會(huì)執(zhí)行 pop(taskQueue)
,將當(dāng)前的任務(wù)從 taskQueue
中彈出。
低優(yōu)先級(jí)任務(wù)重啟
上一步中說(shuō)道一個(gè)低優(yōu)先級(jí)任務(wù)從 taskQueue
中被彈出。那高優(yōu)先級(jí)任務(wù)執(zhí)行完畢之后,如何重啟回之前的低優(yōu)先級(jí)任務(wù)呢?
關(guān)鍵是在 commitRootImpl
函數(shù)
var remainingLanes = mergeLanes(finishedWork.lanes, finishedWork.childLanes); markRootFinished(root, remainingLanes); ... ensureRootIsScheduled(root, now());
markRootFinished
函數(shù)剛剛上面說(shuō)了是釋放已完成任務(wù)所占用的賽道,那也就是說(shuō)未完成任務(wù)依然會(huì)占用其賽道,所以我們可以重新調(diào)用 ensureRootIsScheduled
發(fā)起一次新的調(diào)度,去重啟低優(yōu)先級(jí)任務(wù)的執(zhí)行。我們可以看下重啟部分的判斷
var nextLanes = getNextLanes( root, root === workInProgressRoot ? workInProgressRootRenderLanes : NoLanes ); // 如果 nextLanes 為 NoLanes,就證明所有任務(wù)都執(zhí)行完畢了 if (nextLanes === NoLanes) { ... root.callbackNode = null; root.callbackPriority = NoLane; // 只要 nextLanes 為 NoLanes,就可以結(jié)束調(diào)度了 return; } // 如果 nextLanes 不為 NoLanes,就代表還有任務(wù)未執(zhí)行完,也就是那些被打斷的低優(yōu)先級(jí)任務(wù) ...
饑餓任務(wù)問(wèn)題
上面說(shuō)到,在高優(yōu)先級(jí)任務(wù)執(zhí)行完畢之后,低優(yōu)先級(jí)任務(wù)就會(huì)被重啟,但假設(shè)如果持續(xù)有高優(yōu)先級(jí)任務(wù)持續(xù)進(jìn)來(lái),我的低優(yōu)先級(jí)任務(wù)豈不是沒(méi)有重啟之日?
所以 react 為了處理解決饑餓任務(wù)問(wèn)題,react 在 ensureRootIsScheduled
函數(shù)開(kāi)始的時(shí)候做了以下處理:(參考markStarvedLanesAsExpired
函數(shù))
var lanes = pendingLanes; while (lanes > 0) { var index = pickArbitraryLaneIndex(lanes); var lane = 1 << index; var expirationTime = expirationTimes[index]; if (expirationTime === NoTimestamp) { if ((lane & suspendedLanes) === NoLanes || (lane & pingedLanes) !== NoLanes) { expirationTimes[index] = computeExpirationTime(lane, currentTime); } } else if (expirationTime <= currentTime) { root.expiredLanes |= lane; } lanes &= ~lane; }
- 遍歷31條賽道,判斷每條賽道的過(guò)期時(shí)間是否為
NoTimestamp
,如果是,且該賽道存在待執(zhí)行的任務(wù),則為該賽道初始化過(guò)期時(shí)間 - 如果該賽道已存在過(guò)期時(shí)間,且過(guò)期時(shí)間已經(jīng)小于當(dāng)前時(shí)間,則代表任務(wù)已過(guò)期,需要將當(dāng)前優(yōu)先級(jí)合并到
expiredLanes
,這樣在下一輪 render 階段就會(huì)以同步優(yōu)先級(jí)調(diào)度當(dāng)前HostRoot
可以參考 render 階段執(zhí)行的函數(shù) performConcurrentWorkOnRoot
中的代碼片段
var exitStatus = shouldTimeSlice(root, lanes) && ( !didTimeout) ? renderRootConcurrent(root, lanes) : renderRootSync(root, lanes);
可以看到只要 shouldTimeSlice
只要返回 false,就會(huì)執(zhí)行 renderRootSync
,也就是以同步優(yōu)先級(jí)進(jìn)入 render 階段。而 shouldTimeSlice
的邏輯也就是剛剛的 expiredLanes
屬性相關(guān)
function shouldTimeSlice(root, lanes) { // 如果 expiredLanes 里面有東西,代表有饑餓任務(wù) if ((lanes & root.expiredLanes) !== NoLanes) { return false; } var SyncDefaultLanes = InputContinuousHydrationLane | InputContinuousLane | DefaultHydrationLane | DefaultLane; return (lanes & SyncDefaultLanes) === NoLanes; }
總結(jié)
react
的優(yōu)先級(jí)機(jī)制在源碼中并不是一個(gè)獨(dú)立的,解耦的模塊,而是涉及到了react整體運(yùn)行的方方面面,最后回歸整理下優(yōu)先級(jí)機(jī)制在源碼中的使用,讓大家對(duì)優(yōu)先級(jí)機(jī)制有一個(gè)更加整體的認(rèn)知。
- 時(shí)間分片。涉及到任務(wù)打斷、根據(jù)優(yōu)先級(jí)計(jì)算分片時(shí)長(zhǎng)
- setState 生成 Update 對(duì)象。每個(gè) Update 對(duì)象里面都有一個(gè) lane 屬性,代表此次更新的優(yōu)先級(jí)
- 高優(yōu)先級(jí)任務(wù)打斷低優(yōu)先級(jí)任務(wù)。每一次調(diào)度都會(huì)對(duì)正在進(jìn)行任務(wù)和當(dāng)前任務(wù)最高優(yōu)先級(jí)做比較,如果不相等,就代表有高優(yōu)先級(jí)任務(wù)進(jìn)來(lái),需要打斷當(dāng)前正在的任務(wù)。
- 低優(yōu)先級(jí)任務(wù)重啟。協(xié)調(diào)
(reconcile)
的下一個(gè)階段是渲染(renderer)
,也就是我們說(shuō)的 commit 階段,在此階段末尾,會(huì)調(diào)用ensureRootIsScheduled
發(fā)起一次新的調(diào)度,執(zhí)行尚未完成的低優(yōu)先級(jí)任務(wù)。 - 饑餓任務(wù)喚醒。每次調(diào)度的開(kāi)始,都會(huì)先檢查下有沒(méi)有過(guò)期任務(wù),如果有的話,下一次就會(huì)以同步優(yōu)先級(jí)進(jìn)行 render 任務(wù)
(reconcile)
,同步優(yōu)先級(jí)就是最高的優(yōu)先級(jí),不會(huì)被打斷
以上就是React狀態(tài)更新的優(yōu)先級(jí)機(jī)制源碼解析的詳細(xì)內(nèi)容,更多關(guān)于React 狀態(tài)更新優(yōu)先級(jí)的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
react-three/postprocessing庫(kù)的參數(shù)中文含義使用解析
這篇文章主要介紹了react-three/postprocessing庫(kù)的參數(shù)中文含義使用總結(jié),本文給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2022-05-05淺談react?16.8版本新特性以及對(duì)react開(kāi)發(fā)的影響
本文主要介紹了react?16.8版本新特性以及對(duì)react開(kāi)發(fā)的影響,文中通過(guò)示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2022-03-03React中的Context應(yīng)用場(chǎng)景分析
這篇文章主要介紹了React中的Context應(yīng)用場(chǎng)景分析,Context 提供了一種在組件之間共享數(shù)據(jù)的方式,而不必顯式地通過(guò)組件樹(shù)的逐層傳遞 props,通過(guò)實(shí)例代碼給大家介紹使用步驟,感興趣的朋友跟隨小編一起看看吧2021-06-06React Hooks - useContetx和useReducer的使用實(shí)例詳解
這篇文章主要介紹了React Hooks - useContetx和useReducer的基本使用,本文通過(guò)實(shí)例代碼給大家講解的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2022-11-11React實(shí)現(xiàn)點(diǎn)擊刪除列表中對(duì)應(yīng)項(xiàng)
本文主要介紹了React 點(diǎn)擊刪除列表中對(duì)應(yīng)項(xiàng)的方法。具有一定的參考價(jià)值,下面跟著小編一起來(lái)看下吧2017-01-01react?express實(shí)現(xiàn)webssh?demo解析
這篇文章主要為大家介紹了詳解react?express實(shí)現(xiàn)webssh?demo解析,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-04-04使用react+redux實(shí)現(xiàn)彈出框案例
這篇文章主要為大家詳細(xì)介紹了使用react+redux實(shí)現(xiàn)彈出框案例,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2022-08-08