由於有裁員訊息流出+被打C的雙重衝擊,只好儘量在被裁之前惡補一波自己的未知領域,隨時做好準備
本文是自己在閱讀完react fiber的主流程及部分功能模組的一篇個人總結,有些措辭、理解上難免有誤,若各位讀到這篇文章,請不要吝嗇你的評論,請提出任何質疑、批評,可以逐行提出任何問題
閱讀之前需要了解react和fiber的基本知識,如fiber的連結串列結構,fiber的遍歷,react的基本用法,react渲染兩個階段(reconcile和commit),fiber的副作用(effectTag)
個人推薦的fiber閱讀方法:
- facebook/react
- 初級入門最佳文章:司徒正美:React Fiber架構
正美大哥的其它fiber文章,需要邊看原始碼邊看文章,只看文章會十分費解,不是他講的不好,而是react細節太多又重要,講太細更加雲裡霧裡 - 排除ref、context等細節,排除suspense、error boundary等模組,reconcile+commit在react中我認為是比較簡單的,最好自己看看原始碼,不懂再參考:onefinis:React Fiber 原始碼理解
- reconcile+commit嫌原始碼太長太雜,直接參考:Luminqi/learn-react,帶講解,且程式碼中影響主流程的原始碼已經被刪光,需要注意其程式碼設定是expirationTime越小優先順序越高,與最新版本的react相反
react排程器任務流程概括
react為實現併發模式(非同步渲染)設計了一個帶優先順序的任務系統,如果我們不知道這個任務系統的運作方式,永遠也不會真正瞭解react,接下來的講解預設開啟react併發模式
首先併發模式的功能:
- 時間分片(time slice),保證動畫流暢,保證互動響應,提升使用者體驗
- 任務的優先順序渲染
先從2說起,因為我們要對react有一個全域性的理解
首先,要知道排程演算法包含一切,每一個排程任務,都需要完成reconcile和commit的流程,因此ReactFiberScheduler.js即為react最核心的模組,完成任務排程全靠他;任務排程需要有一個排程器,細節請移步下文中的Scheduler.js;這個排程器按優先順序順序儲存著多個任務,firstCallbackNode為當前任務,從最高優先順序任務開始,如圖所示:
大家想象一下,排程器要實現任務的優先順序排程,當高優先順序任務來臨時,當前執行的任務(firstCallbackNode)需要打斷,讓位給高優先順序任務,這個過程必須在macrotask中完成,為什麼?首先requestIdleCallback
為macrotask,而且這樣的打斷才是我們需要的,因為如果在主執行緒來排程,使用者的互動會被js執行卡住,你想打斷都打斷不了
我們一般會在哪呼叫setState?
componentWillMount
中呼叫setState;該生命週期的執行會執行在reconcile階段,不加入任務排程器componentDidMount
中呼叫setState;該生命週期的執行會執行在commit階段,react中有一個isRendering
標誌,true表示reconcile+commit正在進行,任務加入排程器需要isRendering
為false,不加入任務排程器- onClick、onChange事件的事件回撥函式中呼叫setState;react將這些事件觸發的更新視為批量更新,排程任務不會加入排程器,而是收集所有的setState,再批量同步更新
又由於初始化渲染不開啟併發模式,因此排程器中只會有三種來源的任務:
- 在非主執行緒(setTimeout、promise等)中使用setState而建立的排程任務
- 手動使用
unstable_scheduleCallback
以呼叫setState而建立的任務 - 在2中,排程器開始執行setState任務,發起的排程任務
在1和3中,setState就真的成了非同步更新了;對於1和3,react也會做一個合併的處理,將所有setState合併,如:
setTimeout(() => {
this.setState({
nums: this.state.nums + 1
})
this.setState({
nums: this.state.nums + 1
})
}, 0)
複製程式碼
若state.nums初始值為0,在非併發模式下,最終會更新到2,因為setState是同步的;而在併發模式下,nums最終仍然為1,因為第二個setState任務無法加入排程器;來源1和來源3都是排程任務,在react排程器中,排程任務不能同時出現兩個或以上;為什麼有這個規則,我們下文再談。原始碼如下:
function scheduleCallbackWithExpirationTime(
root: FiberRoot,
expirationTime: ExpirationTime,
) {
if (callbackExpirationTime !== NoWork) {
// A callback is already scheduled. Check its expiration time (timeout).
// 低階別的任務直接 return
if (expirationTime < callbackExpirationTime) {
// Existing callback has sufficient timeout. Exit.
return;
} else {
if (callbackID !== null) {
// Existing callback has insufficient timeout. Cancel and schedule a
// new one.
// 否則會取消當前,再用新的替代
cancelDeferredCallback(callbackID);
}
}
// The request callback timer is already running. Don't start a new one.
} else {
startRequestCallbackTimer();
}
...
callbackID = scheduleDeferredCallback(performAsyncWork, {timeout});
}
複製程式碼
排程器已經簡單介紹完畢,實現細節請移步下文的Scheduler.js;我們可以把排程器想象成一個黑盒,當我們每呼叫一次併發模式的setState時,就向排程器加入一個任務,它會自動幫我們按優先順序執行
每一個併發模式呼叫的setState都會產生一個排程任務,而每一個排程任務都會完成一次渲染過程,因此我預測在併發模式正式推出後,會有大量文章針對併發模式下的setState的優化文章,至少我們現在可以知道併發模式下的setState可不能濫用;搞清楚了排程器和任務框架我們再來深入一下排程器中的每個任務
排程任務細節
有了上文的整體框架,我覺得這個時候大家可以自己去看看原始碼了,我只要告訴你performAsyncWork
代表向排程器加入一個非同步排程任務,而performSyncWork
代表了主執行緒開始執行一個同步任務,原始碼閱讀就不困難了
任務大流程
先簡單思考一下,一個排程任務需要完成什麼工作:
- 首先,必須完成reconcile+commit的基本功能,也就是完成一次完整的渲染
- 需要實現時間分片功能,這個當然不是簡單地交給
requestIdleCallback
就行了,它只能幫我們儘量分配好時間來執行JS,詳見下文;如果你的callback執行時間太長,它是沒辦法的,因為只要JS執行就會卡互動 - 為了實現時間分片,需要實現打斷功能,commit階段要執行太多副作用,因此我們最好在reconcile中實現打斷
- 為了不卡互動,reconcile執行最好不要超過一幀的時間,需要幀內打斷,此判斷來自Scheduler.js
- 任務執行的好好的,突然來了個高優先順序的大哥,那不好意思,你需要打斷,等大哥執行完,你要重新開始執行,這個功能同樣交給Scheduler.js,在當前任務內,我們只要讓reconcile打斷就行
先驗知識:
- 排程任務從根節點(root)開始,按照深度優先遍歷執行,根節點可以理解為
ReactDOM.render(<App/>, document.getElementById('root'))
所建立,正常來說一個專案應該只有一個 - 一個排程任務可能會包含多個root,執行完高優先順序root,才能執行下一個root,高優先順序root也是可以插隊的
fiber.expirationTime
屬性代表該fiber的優先順序,數值越大,優先順序越高,通過當前時間減超時時間獲得,同步任務優先順序預設為最高- react當前時間是倒著走的,當前時間初始為一個極大值,隨著時間流逝,當前時間越來越小;任務(fiber)的優先順序是根據當前時間減超時時間計算,如當前時間10000,任務超時時間500,當前任務優先順序演算法10000-500=9500;新任務來臨,時間流逝,當前時間變為9500,超時時間不變,新任務優先順序演算法9500-500=9000;新任務優先順序低於原任務;
注意:當時間來到9500時,老任務超時,自動獲得最高優先順序,因為所有新任務除同步任務外優先順序永遠不會超過老任務,react以這套時間規則來防止低優先順序任務一直被插隊 fiber.childExpirationTime
屬性代表該fiber的子節點優先順序,該屬性可以用來判斷fiber的子節點還有沒有任務或比較優先順序,更新時若沒有任務(fiber.childExpirationTime===0
)或者本次渲染的優先順序大於子節點優先順序,那麼不必再往下遍歷 當某元件(fiber)觸發了任務時,會往上遍歷,將fiber.return.expirationTime
和fiber.return.childExpirationTime
全部更新為該元件的expirationTime,一直到root,可以理解為在root上收集更新- fiber有個alternate屬性,實際上是fiber的複製體,同時也指向本體,用於react error boundary踩錯誤,隨時回滾,執行完畢且無誤後本體與複製體同步;在初始化時,react並不建立alternate,而在更新時建立
先看看排程任務的總流程:
從setState開始,區分同步或非同步,同步則直接執行performWork
,非同步則將performWork
加入到macrotask執行(排程器);再根據isYieldy
(是否能打斷,同步則不能打斷,為false) 來呼叫不同的performRoot
迴圈體;圖中綠線代表非同步任務,紅框表示該過程可被打斷;任務未執行完畢的話(被打斷),這裡會重複向排程器加入任務
注意:這裡的打斷代表macrotask中該任務已執行完畢,會把js執行交還給主執行緒,也是使用者互動能得到喘息的唯一機會
再看看performRoot迴圈體:
迴圈判斷是否還有任務以及!(didYield && currentRendererTime > nextFlushedExpirationTime)
,didYield
表示是否已經被排程器叫停;currentRendererTime
可以理解為任務執行的當前時間,通過recomputeCurrentRendererTime()
得到,上文說過,隨著時間流逝,該值越來越小;nextFlushedExpirationTime
表示將要渲染的任務的時間(root.expirationTime);當兩個表示式都為true時,迴圈才退出,didYield
為true說明任務被排程器叫停,需要被打斷,currentRendererTime > nextFlushedExpirationTime
為true表明任務未超時
這裡我認為判斷有些重複,因為排程器已經為我們判斷了是否超時,超時則不會打斷,我認為react在這裡是一個雙保險機制,具體原因未知
進入迴圈,執行performWorkOnRoot()
,這個稍後再講;接下來是findHighestPriorityRoot()
,其實就是找最高優先順序的root,並得到root的expirationTime,root的expirationTime即為將要執行的任務的時間即這裡的nextFlushedExpirationTime
;最後是算當前時間
再看看performWorkOnRoot()
:
我已合併同步非同步的情況,綠線表示非同步多出來的部分;程式碼很簡單,就是判斷finishedWork
是否為空,為空則renderRoot()
,不為空則completeRoot()
,這裡renderRoot
即為reconcile過程,completeRoot
即為commit過程,接下來看reconcile過程
reconcile
renderRoot
其實只做兩件事:
- 執行一個
workLoop
迴圈體 - 判斷
nextUnitOfWork
是否為空,若不為空則任務未完成,下次再繼續,為空則表明reconcile完成,賦值root.finishedWork
,這時候才能commit
workLoop迴圈體:
看的出來workLoop
即為迴圈求nextUnitOfWork
的過程,直到nextUnitOfWork
為空或者被打斷;nextUnitOfWork
是一個全域性變數,就是遍歷所在的fiber,那麼workLoop
就是不斷地遍歷,求出下一個fiber;先執行beginWork
,beginWork
做了什麼?
- 如果是初次渲染,需要把fiber的兒子們求出來
- 如果是更新,需要將新的兒子們與原來的兒子們做對比(diff 演算法)
- 如果遍歷到的是ClassComponent型別(元件)fiber,則要初始化元件,求出它的例項以及呼叫
processUpdateQueue
(參考後文)得到state,呼叫render
渲染函式以得到JSX表示的虛擬節點,標記componentDidMount等副作用 - 如果遍歷到的是HostComponent型別(div等)fiber,標記Placement副作用
如果beginWork
的結果為空,說明這個節點已經沒有兒子了,接下來就該輪到completeUnitOfWork
出場了,completeUnitOfWork
需要做到:
- 既然向下遍歷已經到頭了,需要向右遍歷,向右遍歷到頭了,需要向上回朔
- 將有effectTag標記的fiber給連線起來,加速commit過程
- 做好appendChild工作
- 若有事件,需要繫結事件系統,參考後文
commit
reconcile完成以後,接下來再回到performWorkOnRoot
中的commitRoot
,主要工作如下:
- 按照reconcile連線的effect順序來遍歷fiber
- 處理被標記的fiber的effectTag,如:Placement、Deletion、Snapshot等
react排程任務細節總結完畢,我並沒有說太多reconcile和commit的細節,因為我認為這部分寫多了就不叫總結了,遠不如自己讀來的清楚
一個不錯的比喻
將整個react專案比作一個大型礦場,使用者是老闆,排程器是包工頭,排程任務是礦工,不同的礦場代表著不同的root;一個專案只能有一個礦坑(nextUnitOfWork
),虛線底部代表礦已挖完,reconcile結束
- 老闆發出讓礦工挖礦的指令,由包工頭來排程礦工,包工頭會按礦工的優先順序順序來挖礦,最高階的礦工先挖礦,若來了更高階的礦工,當前礦工停止工作,讓位給他
- 礦工開始挖礦時,會選擇優先順序高的礦場來挖礦,正如排程任務從高優先順序的root開始排程
- 礦工需要定時休息(任務超過1幀時間),休息好了會接著原來的礦坑來挖礦
- 所有礦場只會有一個礦坑,如果中途替換了不同礦工挖礦,礦工會新開一個礦坑來挖礦
疑問
- 為什麼排程器內只能有一個排程任務?
因為一個礦坑只產出一種礦,不同礦工來挖同一個礦坑,有的礦工挖的是金礦,有的礦工挖的是煤礦,不允許;這裡可能也有react15中setState合併的考慮
- 為什麼commit階段不能打斷?
commit階段要執行componentDidMount這種react完全失控的副作用,以及其它生命週期,當然不能打斷,不然打斷再執行,豈不是會重複呼叫多次? 在reconcile階段,react同樣避免呼叫任何失控的程式碼,如componentWillReceiveProps,componentWillReceiveProps,使用者在這些生命週期裡面呼叫setState,reconcile被打斷後重新開始豈不是要呼叫多次setState?
- 為什麼排程任務要從root開始排程?
如果從目標fiber開始更新,如這裡的fiber2,那麼我們的礦坑就可以從fiber2開始挖,節省了時間;但是你沒有想過,root的優先順序是會更新的,如果這時候fiber3擁有了更高優先順序,那麼會從fiber3開始遍歷,由於遍歷只能向下或向右,我們會忽視fiber2的更新;所以不如把所有更新提到root,這樣唯一的壞處就是被打斷之後要從root開始遍歷,但是至少不會漏掉更新
思考
- react有個致命缺點,到了react16依然沒有改進,那就是如果我們有1萬個節點,只變動其中的一個,那麼react會在reconcile過程遍歷1萬次以上,即使最後commit只做一次,以及dom變更只做一次,但是reconcile的開銷太沒必要了,vue在這點上完爆了react
- 瀏覽器的改進會導致現有的react fiber架構崩潰,只需要做一個改進:JS執行不阻塞使用者互動,動畫,重排與重繪
- react是否能使用web worker來改進排程器,排程器始終是個單執行緒任務執行器,如果我們用web worker來排程任務更能使瀏覽器的效能發揮到極致,當然第一個前提就是我們的礦坑(nextUnitOfWork)不能是個全域性變數
- react併發模式有一個說不上是bug,但是對於使用者體驗來說是bug的問題
xilixjd/xjd-react-study 這個頁面,當使用者在輸入框中輸入123時,輸入框最終顯示結果為3,或13,原因是reconcile + commit過程太慢,使用者在輸入1時,頁面上輸入框的1都沒刷出來,使用者又輸了2,剛刷出1時,使用者又輸入了3,所以setState接收到的可能是單獨的1,單獨的2,單獨的3或13 - 再有,手動呼叫
unstable_scheduleCallback
時的timeout值非常有講究,當使用者以滾鍵盤的極快速度輸入1-9時,timeout值設得過低,中間很多數字將不會被渲染! 舉例來說,當輸入1時,以unstable_scheduleCallback
來呼叫setState,排程器中存在的任務是setState任務,然後setState任務又建立了一個排程任務,這個排程任務不斷地打斷重連,我們的互動得到喘息,輸入了2,3,4...排程器中又放入了多個setState任務,因為按得太快,這些任務在排程器中被連線到了一起;在第一個任務打斷重連完畢後,接下來的幾個setState任務全部執行並轉成了排程任務,由於這幾個排程任務expirationTime相等,執行的卻是不同的setState任務,因此排程任務被合併,只會剩下最後一個執行的排程任務; 不過,當timeout值設定得夠大時,問題將得到解決,因為這時候加入排程器的setState任務的expirationTime會非常大,它們的執行會非常靠後,在它們建立的每一個排程任務執行完之後,因此輸入框的數字將渲染得很完整,不過依然無法擺脫4的問題
TODO
- react hooks
- react suspense,error boundary
- context,ref
react事件系統
react在這部分的內容很多很雜,但是我認為對主流程而言沒必要講的太細,況且我也沒看太仔細,這裡更多細節只需要參考這篇文章React事件系統和原始碼淺析 - 掘金
簡單來說,react實現了一套事件系統;在更新props階段,就為所有擁有事件回撥的fiber繫結好事件(react事件系統),事件繫結在document上;觸發事件時,進入事件系統,事件系統建立一個SyntheticEvent
用來代替原生的e物件;接著,以冒泡/捕獲的順序收集所有fiber和其中的事件回撥;再按冒泡/捕獲順序觸發繫結在fiber上的回撥函式
這裡需要注意幾點:
- 在為document繫結事件的時候,有的事件會繫結幾個附加的事件,如:當為input繫結change事件時,會將focus事件一同繫結,用意未知
- 強互動事件(click,change)觸發的是同步work,同步work會阻塞執行緒,也就是說同步work執行完才能開始新互動,但是這段在原始碼
interactiveUpdates()
裡的判斷讓人費解,找不到其場景
if (
!isBatchingUpdates &&
!isRendering &&
lowestPriorityPendingInteractiveExpirationTime !== NoWork
) {
// Synchronously flush pending interactive updates.
performWork(lowestPriorityPendingInteractiveExpirationTime, false);
lowestPriorityPendingInteractiveExpirationTime = NoWork;
}
複製程式碼
Scheduler.js —— 實現了requestIdleCallback的polyfill + 優先順序任務的功能
關鍵詞:requestAnimationFrame、frameDeadline、activeFrameTime、timeout、unstable_scheduleCallback、unstable_cancelCallback、unstable_shouldYield
簡介
核心模組。react fiber的任務排程全靠它,我認為搞懂這個模組才能搞懂react schedule的過程,unstable_scheduleCallback
、unstable_cancelCallback
、unstable_shouldYield
,三個api能夠分別實現將任務加入任務列表,將任務從任務列表中刪除,以及判斷任務是否應該被打斷
背景
主要實現方法是運用requestAnimationFrame + MessageChannel + 雙向連結串列的插入排序,最後暴露出unstable_scheduleCallback
和unstable_shouldYield
兩個api。
對第一位的理解需要看一下這篇文章,簡單來說是螢幕顯示是顯示器在不斷地重新整理影象,如60Hz的螢幕,每16ms重新整理一次,而1幀代表的是一個靜止的畫面,若一個dom元素從左到右移動,而我們需要這個dom每一幀向右移動1px,60Hz的螢幕,我們需要在16ms以內,完成向右移動的js執行和dom繪製,這樣在第二幀(17ms時)開始的時候,dom已經右移了1px,並且被螢幕給刷了出來,我們的眼睛才會感覺到動畫的連續性,也就是常說的不掉幀。requestAnimationFrame
則給了我們十分精確且可靠的服務。
requestAnimationFrame如何模擬requestIdleCallback?
requestIdleCallback
的功能是在每一幀的空閒時間(完成dom繪製、動畫等之後)來執行js,若這一幀的空閒時間不足,則分配到下一幀執行,再不足,分配到下下幀完成,直到超過規定的timeout時間,則直接執行js。requestAnimationFrame
能儘量保證回撥函式在一幀內執行一次且dom繪製一次,這樣也保證了動畫等效果的流暢度,然而卻沒有超時執行機制,react polyfill的主要是超時功能。
requestAnimationFrame
通常的用法:
function callback(currentTime) {
// 動畫操作
...
window.requestAnimationFrame(callback)
}
window.requestAnimationFrame(callback)
複製程式碼
其代表的是每一幀都儘量執行一次callback,並完成動畫繪製,若執行不完,也沒辦法,就掉幀。
Scheduler.js使用animationTick
作為requestAnimationFrame
的callback,用以計算frameDeadline
和呼叫傳入的回撥函式,在react中即為排程函式;frameDeadline
表示的是執行到當前幀的幀過期時間,計算方法是當前時間 + activeFrameTime
,activeFrameTime
表示的是一幀的時間,預設為33ms,但是會根據裝置動態調整,比如在重新整理頻率更高的裝置上,連續執行兩幀的當前時間比執行到該幀的過期時間frameDeadline
都小,說明我們一幀中的js任務耗時也小,一幀時間充足且requestAnimationFrame
呼叫比預設的33ms頻繁,那麼activeFrameTime
會降低以達到最佳效能
有了frameDeadline
與使用者自定義的過期時間timeoutTime
,那麼我們很容易得到polyfill requestIdleCallback的原理:使用者定義的callback在這一幀有空就去執行,超過幀過期時間frameDeadline
就到下一幀去執行,你可以超過幀過期時間,但是你不能超過使用者定義的timeoutTime
,一旦超過,我啥也不管,直接執行callback。
如何實現優先順序任務?
Scheduler.js將每一次unstable_scheduleCallback
的呼叫根據使用者定義的timeout來為任務分配優先順序,timeout越小,優先順序越高。具體實現為:用雙向連結串列結構來表示任務列表,且按優先順序從高到低的順序進行排列,當某個任務插入時,從頭結點開始迴圈遍歷,若遇到某個任務結點node的expirationTime > 插入任務的expirationTime,說明插入任務比node優先順序高,則退出迴圈,並在node前插入,expirationTime = 當前時間 + timeout;這樣就實現了按優先順序排序的任務插入功能,animationTick
會迴圈呼叫這些任務連結串列。
重難點
function unstable_shouldYield() {
return (
!currentDidTimeout &&
((firstCallbackNode !== null &&
firstCallbackNode.expirationTime < currentExpirationTime) ||
shouldYieldToHost())
);
}
shouldYieldToHost = function() {
return frameDeadline <= getCurrentTime();
};
複製程式碼
unstable_shouldYield
被用來判斷在任務列表中是否有更高階的任務,在react中用來判斷是否能打斷當前任務,是schedule中的一個核心api。
首先判斷currentDidTimeout
,currentDidTimeout
為false說明任務沒有過期,大家要知道過期任務擁有最高優先順序,那麼即使有更高階的任務依然無法打斷,直接return false;
再判斷firstCallbackNode.expirationTime < currentExpirationTime
,這裡實際上是照顧一種特殊的情況,那就是一個最高優先順序的任務插入之後,低優先順序的任務還在執行中,這種情況是仍然需要打斷的;這裡firstCallbackNode
其實是那個插入的高優先順序任務,而currentExpirationTime
其實是上一個任務的expirationTime,只是還沒結算
最後是一個shouldYieldToHost()
,很簡單,就是看任務在幀內是否過期,注意到這邊任務幀內過期的話是return true,代表直接就能被打斷;
ReactUpdateQueue.js —— 用來更新state的模組
關鍵詞:enqueueUpdate、processUpdateQueue
react15中,所有經互動事件觸發的setState更新都會被收集到dirtyComponents,收集好了再批量更新;react16由於加入了優先順序策略,在排程時連setState操作都被賦予不同的優先順序,在同一元件針對帶優先順序的排程任務及setState操作,是該模組的核心功能
首先貼兩個資料結構(已刪去部分不關注的屬性):
export type Update<State> = {
expirationTime: ExpirationTime,
payload: any,
callback: (() => mixed) | null,
next: Update<State> | null,
nextEffect: Update<State> | null,
};
export type UpdateQueue<State> = {
baseState: State,
// 頭節點
firstUpdate: Update<State> | null,
// 尾節點
lastUpdate: Update<State> | null,
// callback 處理
firstEffect: Update<State> | null,
lastEffect: Update<State> | null,
};
複製程式碼
fiber上有個updateQueue
屬性,就是來自上述資料結構。每次呼叫setState的時候,會新建一個updateQueue
,queue中儲存了baseState
,用於記錄state,該屬性服務於優先順序排程,後面會說;另外記錄頭節點、尾節點及用於callback的effect頭尾指標;還有以連結串列形式連線的update,如圖所示:
每當呼叫一次setState,會呼叫enqueueUpdate
,就會在連結串列之後插入一個update,這個插入是無序的,然而不同的update是帶優先順序的,用一個屬性expirationTime來表示,payload即為呼叫setState的第一個引數。
當排程任務依次執行時,會呼叫processUpdateQueue
計算最終的state,我們不要忘了排程任務是帶有優先順序的任務,執行的時候有先後順序,對應的是processUpdateQueue
的先後執行順序;而update也是優先順序任務的一部分,當我們按連結串列順序從頭到尾執行時,需要優先執行高優先順序的update,跳過低優先順序的update;react的註釋為我們闡明瞭這一過程:
假設有一updateQueue為A1 - B2 - C1 - D2;
A1、B2等代表一個update,其中字母代表state,數字大小代表優先順序,1為高優先順序;
排程任務按高低優先順序依次執行,第一次排程是高優先順序任務,從頭結點firstUpdate開始處理,processUpdateQueue會跳過低優先順序的update;
則執行的update為A1 - C1,本次排程得到的最終state為AC,baseState為A,queue的firstUpdate指標指向B2,以供下次排程使用;
第二次排程是低優先順序任務,此時firstUpdate指向B2,則從B2開始,執行的update為 B2 - C1 - D2,最終state將與baseState:A合併,得到ABCD
以上即為processUpdateQueue
的處理過程,我們需要注意幾點:
processUpdateQueue
從頭結點firstUpdate
開始遍歷update,並對state進行合併 對於低優先順序的update,遍歷時會跳過- 當遇到有被跳過的update時,
baseState
會定格在被跳過的update之前的resultState baseState
主要作用在於記錄好被跳過的update之前的state,以便在下一次更加低優先順序的排程任務時合併state- 所有排程任務完成後,
firstUpdate
和lastUpdate
指向null,updateQueue
完成使命
再看一個例子:
A1-B1-C2-D3-E2-F1
第一次排程:baseState:AB,resultState:ABF,firstUpdate:C2
第二次排程:baseState:ABC,resultState:ABCEF,firstUpdate:D3
第三次排程:baseState:ABC,resultState:ABCDEF,firstUpdate:null