react fiber 主流程及功能模組梳理

xilixjd發表於2019-02-23

由於有裁員訊息流出+被打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併發模式

首先併發模式的功能:

  1. 時間分片(time slice),保證動畫流暢,保證互動響應,提升使用者體驗
  2. 任務的優先順序渲染

先從2說起,因為我們要對react有一個全域性的理解

首先,要知道排程演算法包含一切,每一個排程任務,都需要完成reconcile和commit的流程,因此ReactFiberScheduler.js即為react最核心的模組,完成任務排程全靠他;任務排程需要有一個排程器,細節請移步下文中的Scheduler.js;這個排程器按優先順序順序儲存著多個任務,firstCallbackNode為當前任務,從最高優先順序任務開始,如圖所示:

排程器
排程器,雙向迴圈連結串列結構,這些任務存在於macrotask

大家想象一下,排程器要實現任務的優先順序排程,當高優先順序任務來臨時,當前執行的任務(firstCallbackNode)需要打斷,讓位給高優先順序任務,這個過程必須在macrotask中完成,為什麼?首先requestIdleCallback為macrotask,而且這樣的打斷才是我們需要的,因為如果在主執行緒來排程,使用者的互動會被js執行卡住,你想打斷都打斷不了

我們一般會在哪呼叫setState?

  1. componentWillMount中呼叫setState;該生命週期的執行會執行在reconcile階段,不加入任務排程器
  2. componentDidMount中呼叫setState;該生命週期的執行會執行在commit階段,react中有一個isRendering標誌,true表示reconcile+commit正在進行,任務加入排程器需要isRendering為false,不加入任務排程器
  3. onClick、onChange事件的事件回撥函式中呼叫setState;react將這些事件觸發的更新視為批量更新,排程任務不會加入排程器,而是收集所有的setState,再批量同步更新

又由於初始化渲染不開啟併發模式,因此排程器中只會有三種來源的任務:

  1. 在非主執行緒(setTimeout、promise等)中使用setState而建立的排程任務
  2. 手動使用unstable_scheduleCallback以呼叫setState而建立的任務
  3. 在2中,排程器開始執行setState任務,發起的排程任務
    排程器中三種來源的任務
    3000、4000代表優先順序;來源3的任務來自來源2

在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代表了主執行緒開始執行一個同步任務,原始碼閱讀就不困難了

任務大流程

先簡單思考一下,一個排程任務需要完成什麼工作:

  1. 首先,必須完成reconcile+commit的基本功能,也就是完成一次完整的渲染
  2. 需要實現時間分片功能,這個當然不是簡單地交給requestIdleCallback就行了,它只能幫我們儘量分配好時間來執行JS,詳見下文;如果你的callback執行時間太長,它是沒辦法的,因為只要JS執行就會卡互動
  3. 為了實現時間分片,需要實現打斷功能,commit階段要執行太多副作用,因此我們最好在reconcile中實現打斷
  4. 為了不卡互動,reconcile執行最好不要超過一幀的時間,需要幀內打斷,此判斷來自Scheduler.js
  5. 任務執行的好好的,突然來了個高優先順序的大哥,那不好意思,你需要打斷,等大哥執行完,你要重新開始執行,這個功能同樣交給Scheduler.js,在當前任務內,我們只要讓reconcile打斷就行

先驗知識:

  1. 排程任務從根節點(root)開始,按照深度優先遍歷執行,根節點可以理解為ReactDOM.render(<App/>, document.getElementById('root'))所建立,正常來說一個專案應該只有一個
  2. 一個排程任務可能會包含多個root,執行完高優先順序root,才能執行下一個root,高優先順序root也是可以插隊的
  3. fiber.expirationTime屬性代表該fiber的優先順序,數值越大,優先順序越高,通過當前時間減超時時間獲得,同步任務優先順序預設為最高
  4. react當前時間是倒著走的,當前時間初始為一個極大值,隨著時間流逝,當前時間越來越小;任務(fiber)的優先順序是根據當前時間減超時時間計算,如當前時間10000,任務超時時間500,當前任務優先順序演算法10000-500=9500;新任務來臨,時間流逝,當前時間變為9500,超時時間不變,新任務優先順序演算法9500-500=9000;新任務優先順序低於原任務;
    注意:當時間來到9500時,老任務超時,自動獲得最高優先順序,因為所有新任務除同步任務外優先順序永遠不會超過老任務,react以這套時間規則來防止低優先順序任務一直被插隊
  5. fiber.childExpirationTime屬性代表該fiber的子節點優先順序,該屬性可以用來判斷fiber的子節點還有沒有任務或比較優先順序,更新時若沒有任務(fiber.childExpirationTime===0)或者本次渲染的優先順序大於子節點優先順序,那麼不必再往下遍歷 當某元件(fiber)觸發了任務時,會往上遍歷,將fiber.return.expirationTimefiber.return.childExpirationTime全部更新為該元件的expirationTime,一直到root,可以理解為在root上收集更新
  6. fiber有個alternate屬性,實際上是fiber的複製體,同時也指向本體,用於react error boundary踩錯誤,隨時回滾,執行完畢且無誤後本體與複製體同步;在初始化時,react並不建立alternate,而在更新時建立

先看看排程任務的總流程:

排程任務大流程
排程任務大流程

從setState開始,區分同步或非同步,同步則直接執行performWork,非同步則將performWork加入到macrotask執行(排程器);再根據isYieldy(是否能打斷,同步則不能打斷,為false) 來呼叫不同的performRoot迴圈體;圖中綠線代表非同步任務,紅框表示該過程可被打斷;任務未執行完畢的話(被打斷),這裡會重複向排程器加入任務

注意:這裡的打斷代表macrotask中該任務已執行完畢,會把js執行交還給主執行緒,也是使用者互動能得到喘息的唯一機會

再看看performRoot迴圈體:

performRoot迴圈體
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()

performWorkOnRoot
performWorkOnRoot

我已合併同步非同步的情況,綠線表示非同步多出來的部分;程式碼很簡單,就是判斷finishedWork是否為空,為空則renderRoot(),不為空則completeRoot() ,這裡renderRoot即為reconcile過程,completeRoot即為commit過程,接下來看reconcile過程

reconcile

renderRoot其實只做兩件事:

  1. 執行一個workLoop迴圈體
  2. 判斷nextUnitOfWork是否為空,若不為空則任務未完成,下次再繼續,為空則表明reconcile完成,賦值root.finishedWork,這時候才能commit

workLoop迴圈體:

workLoop迴圈體
workLoop迴圈體

看的出來workLoop即為迴圈求nextUnitOfWork的過程,直到nextUnitOfWork為空或者被打斷;nextUnitOfWork是一個全域性變數,就是遍歷所在的fiber,那麼workLoop就是不斷地遍歷,求出下一個fiber;先執行beginWorkbeginWork做了什麼?

  1. 如果是初次渲染,需要把fiber的兒子們求出來
  2. 如果是更新,需要將新的兒子們與原來的兒子們做對比(diff 演算法)
  3. 如果遍歷到的是ClassComponent型別(元件)fiber,則要初始化元件,求出它的例項以及呼叫processUpdateQueue(參考後文)得到state,呼叫render渲染函式以得到JSX表示的虛擬節點,標記componentDidMount等副作用
  4. 如果遍歷到的是HostComponent型別(div等)fiber,標記Placement副作用

如果beginWork的結果為空,說明這個節點已經沒有兒子了,接下來就該輪到completeUnitOfWork出場了,completeUnitOfWork需要做到:

  1. 既然向下遍歷已經到頭了,需要向右遍歷,向右遍歷到頭了,需要向上回朔
  2. 將有effectTag標記的fiber給連線起來,加速commit過程
  3. 做好appendChild工作
  4. 若有事件,需要繫結事件系統,參考後文

commit

reconcile完成以後,接下來再回到performWorkOnRoot中的commitRoot,主要工作如下:

  1. 按照reconcile連線的effect順序來遍歷fiber
  2. 處理被標記的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結構
    fiber結構

如果從目標fiber開始更新,如這裡的fiber2,那麼我們的礦坑就可以從fiber2開始挖,節省了時間;但是你沒有想過,root的優先順序是會更新的,如果這時候fiber3擁有了更高優先順序,那麼會從fiber3開始遍歷,由於遍歷只能向下或向右,我們會忽視fiber2的更新;所以不如把所有更新提到root,這樣唯一的壞處就是被打斷之後要從root開始遍歷,但是至少不會漏掉更新

思考

  1. react有個致命缺點,到了react16依然沒有改進,那就是如果我們有1萬個節點,只變動其中的一個,那麼react會在reconcile過程遍歷1萬次以上,即使最後commit只做一次,以及dom變更只做一次,但是reconcile的開銷太沒必要了,vue在這點上完爆了react
  2. 瀏覽器的改進會導致現有的react fiber架構崩潰,只需要做一個改進:JS執行不阻塞使用者互動,動畫,重排與重繪
  3. react是否能使用web worker來改進排程器,排程器始終是個單執行緒任務執行器,如果我們用web worker來排程任務更能使瀏覽器的效能發揮到極致,當然第一個前提就是我們的礦坑(nextUnitOfWork)不能是個全域性變數
  4. react併發模式有一個說不上是bug,但是對於使用者體驗來說是bug的問題
    xilixjd/xjd-react-study 這個頁面,當使用者在輸入框中輸入123時,輸入框最終顯示結果為3,或13,原因是reconcile + commit過程太慢,使用者在輸入1時,頁面上輸入框的1都沒刷出來,使用者又輸了2,剛刷出1時,使用者又輸入了3,所以setState接收到的可能是單獨的1,單獨的2,單獨的3或13
  5. 再有,手動呼叫unstable_scheduleCallback時的timeout值非常有講究,當使用者以滾鍵盤的極快速度輸入1-9時,timeout值設得過低,中間很多數字將不會被渲染! 舉例來說,當輸入1時,以unstable_scheduleCallback來呼叫setState,排程器中存在的任務是setState任務,然後setState任務又建立了一個排程任務,這個排程任務不斷地打斷重連,我們的互動得到喘息,輸入了2,3,4...排程器中又放入了多個setState任務,因為按得太快,這些任務在排程器中被連線到了一起;在第一個任務打斷重連完畢後,接下來的幾個setState任務全部執行並轉成了排程任務,由於這幾個排程任務expirationTime相等,執行的卻是不同的setState任務,因此排程任務被合併,只會剩下最後一個執行的排程任務; 不過,當timeout值設定得夠大時,問題將得到解決,因為這時候加入排程器的setState任務的expirationTime會非常大,它們的執行會非常靠後,在它們建立的每一個排程任務執行完之後,因此輸入框的數字將渲染得很完整,不過依然無法擺脫4的問題

TODO

  1. react hooks
  2. react suspense,error boundary
  3. context,ref

react事件系統

react在這部分的內容很多很雜,但是我認為對主流程而言沒必要講的太細,況且我也沒看太仔細,這裡更多細節只需要參考這篇文章React事件系統和原始碼淺析 - 掘金

簡單來說,react實現了一套事件系統;在更新props階段,就為所有擁有事件回撥的fiber繫結好事件(react事件系統),事件繫結在document上;觸發事件時,進入事件系統,事件系統建立一個SyntheticEvent用來代替原生的e物件;接著,以冒泡/捕獲的順序收集所有fiber和其中的事件回撥;再按冒泡/捕獲順序觸發繫結在fiber上的回撥函式

這裡需要注意幾點:

  1. 在為document繫結事件的時候,有的事件會繫結幾個附加的事件,如:當為input繫結change事件時,會將focus事件一同繫結,用意未知
  2. 強互動事件(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_scheduleCallbackunstable_cancelCallbackunstable_shouldYield,三個api能夠分別實現將任務加入任務列表,將任務從任務列表中刪除,以及判斷任務是否應該被打斷

背景

主要實現方法是運用requestAnimationFrame + MessageChannel + 雙向連結串列的插入排序,最後暴露出unstable_scheduleCallbackunstable_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表示的是執行到當前幀的幀過期時間,計算方法是當前時間 + activeFrameTimeactiveFrameTime表示的是一幀的時間,預設為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。

首先判斷currentDidTimeoutcurrentDidTimeout為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,如圖所示:

updateQueue
updateQueue

每當呼叫一次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的處理過程,我們需要注意幾點:

  1. processUpdateQueue從頭結點firstUpdate開始遍歷update,並對state進行合併 對於低優先順序的update,遍歷時會跳過
  2. 當遇到有被跳過的update時,baseState會定格在被跳過的update之前的resultState
  3. baseState主要作用在於記錄好被跳過的update之前的state,以便在下一次更加低優先順序的排程任務時合併state
  4. 所有排程任務完成後,firstUpdatelastUpdate指向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

相關文章