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

因爲有裁人消息流出+被打C的雙重衝擊,只好儘可能在被裁以前惡補一波本身的未知領域,隨時作好準備html

本文是本身在閱讀完react fiber的主流程及部分功能模塊的一篇我的總結,有些措辭、理解上不免有誤,若各位讀到這篇文章,請不要吝嗇你的評論,請提出任何質疑、批評,能夠逐行提出任何問題vue

閱讀以前須要瞭解react和fiber的基本知識,如fiber的鏈表結構,fiber的遍歷,react的基本用法,react渲染兩個階段(reconcile和commit),fiber的反作用(effectTag)node

我的推薦的fiber閱讀方法:react

  • 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併發模式git

首先併發模式的功能:github

  1. 時間分片(time slice),保證動畫流暢,保證交互響應,提高用戶體驗
  2. 任務的優先級渲染

先從2提及,由於咱們要對react有一個全局的理解web

首先,要知道調度算法包含一切,每個調度任務,都須要完成reconcile和commit的流程,所以ReactFiberScheduler.js即爲react最核心的模塊,完成任務調度全靠他;任務調度須要有一個調度器,細節請移步下文中的Scheduler.js;這個調度器按優先級順序保存着多個任務,firstCallbackNode爲當前任務,從最高優先級任務開始,如圖所示: 算法

調度器
調度器,雙向循環鏈表結構,這些任務存在於macrotask

你們想象一下,調度器要實現任務的優先級調度,當高優先級任務來臨時,當前運行的任務(firstCallbackNode)須要打斷,讓位給高優先級任務,這個過程必須在macrotask中完成,爲何?首先requestIdleCallback爲macrotask,並且這樣的打斷纔是咱們須要的,由於若是在主線程來調度,用戶的交互會被js運行卡住,你想打斷都打斷不了api

咱們通常會在哪調用setState?promise

  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;
A一、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

相關文章
相關標籤/搜索