React Fiber原始碼分析 第四篇(歸納總結)

菜的黑人牙膏發表於2019-01-31

系列文章

React Fiber原始碼分析 第一篇
React Fiber原始碼分析 第二篇(同步模式)
React Fiber原始碼分析 第三篇(非同步狀態)
React Fiber原始碼分析 第四篇(歸納總結)

前言

React Fiber是React在V16版本中的大更新,利用了閒餘時間看了一些原始碼,做個小記錄~

什麼是Fiber

從開發者角度來看

實際上這次更新對於我們來說影響並不大,只是幾個生命週期改變了(React在版本中的更新簡直做到了像一門語言一樣,完美的相容老版本,底層演算法的大重構對於開發者來說完全透明),新引入的兩個生命週期函式 getDerivedStateFromProps,getSnapshotBeforeUpdate 以及在未來 v17.0 版本中即將被移除的三個生命週期函式componentWillMount,componentWillReeiveProps,componentWillUpdate,目前版本並不會影響原生命週期的使用,但不能和新的生命週期一起使用,也會被標記為不安全,下圖為目前React的流程圖

image

其他的幾乎沒有任何影響,我們還是照常的寫著原來的程式碼,然後我們就感覺到網頁效能更高了一些。

為什麼網頁效能會變高

要回答這個問題,需要回頭看javascript是單執行緒的知識點。

單執行緒一次只能做一件事, 在原來的React中, 如果一次更新的時間比較長,那麼使用者就會感覺到卡頓,也就是丟幀了。

打個比方, 假如我現在要更新1000個元件(往大了說),每個元件平均花時間1ms,那麼在1s內,瀏覽器的整個執行緒都被阻塞了,這時候使用者在input上的任何操作都不會有反應,等到更新完畢,介面上突的一下就顯示了原來使用者的輸入,這個體驗是非常差的。這裡借用官方一張圖, Fiber之前的版本就是這樣,呼叫棧非常深

image

那麼Fiber,現在是怎麼做呢?

Fiber實際上是把一次更新拆成一個個的單元任務,每次做完一個單元任務後,就詢問是否有更高的優先順序任務,有就去執行,回頭再來幹這件事,如圖

image

那麼就明白了,Fiber是一個任務調和器!, 同樣,我們根據這個來分析Fiber具體做了什麼

Fiber具體做了什麼

首先,要做到這樣的效果,那麼就需要有以下的功能:

  1. 任務可分片 (拆分任務)
  2. 任務可中斷 (執行另一個任務後, 可以回頭繼續執行未完成的任務)
  3. 具備優先順序 (哪個任務先執行)

任務可分片

在React中,無論是state還是props的更新, 最後都操作在JSX的標籤上
利用這種天然友好的表達,直接把每一個標籤當成一個任務分片如:div、p1、p2、span都是一個任務分片

<div>
  <p>p1</p>
  <p>
    <span>p2</span>
  </p>
</div>
複製程式碼

當然, 還要從標籤轉換成VDOM,再轉成Fiber,才是一個真正的任務片,如圖:

React Fiber原始碼分析 第四篇(歸納總結)

fiber的資料結構

React Fiber原始碼分析 第四篇(歸納總結)

任務可中斷

Fiber之前React是通過棧排程器進行遞迴更新,畢竟標籤化是天然巢狀的,對遞迴友好,但是遞迴不好break和continue

從大遞迴到大迴圈

Fiber則是以連結串列的形式來進行逐步更新(深度優先遍歷演算法),連結串列對break和continue友好Fiber節點擁有return, child, sibling三個屬性,分別對應父節點, 第一個孩子, 它右邊的兄弟,

React Fiber原始碼分析 第四篇(歸納總結)

(圖來自網路,侵刪)

如何回到中斷
任務中斷,執行高優先順序任務後如何回來被中斷的任務

React內部維護一個任務連結串列,每次某個任務結束後都會刪除已完成的任務並繼續執行其他可執行的任務,每個任務都有一個finishedWork屬性,如果該屬性不為null,則說明更新完畢,只差commit render階段

React Fiber原始碼分析 第四篇(歸納總結)

回到中斷任務後,如何從中斷的任務片開始

這個主要依賴於fiber中的兩個屬性expirationTime和childExpirationTime,當某個fiber被執行完畢後,會把expirationTime設為NoWork,即被打斷後可以通過該屬性判斷任務碎片是否 需要執行

this.expirationTime = NoWork  // 任務優先順序
this.childExpirationTime = NoWork // 子任務片的優先順序
複製程式碼
任務中斷再執行的流程
  1. 通過深度遍歷搜尋演算法對每一個fiber即任務碎片進行更新
  2. 每一個任務碎片完成後會將expirationTime設為NoWork
  3. 假設此時有更高優先順序的任務,則執行更高優先順序任務
  4. 任務執行完成後,會從任務列表中剔除,並繼續執行其他未完成且可以執行的任務。
  5. 回到被打斷任務,可以通過任務的finishWork屬性判斷是否需要執行更新
  6. 根據任務碎片的expirationTime判斷是否需要執行更新
中斷更新階段其他屬性介紹
Alternater

每次更新都不會對fiber直接操作,而是克隆一個作為alternater屬性

updateQueue

更新佇列, 存放更新的資訊

Effect

收集更新資訊,生成真實DOM

具備優先順序

每個Root任務\更新任務\fiber都具有expirationTime屬性,該屬性即為優先順序expirationTime越小,優先順序越高,同步模式下該值為0, 每個層級的任務都是以連結串列的形式存在

React Fiber原始碼分析 第四篇(歸納總結)

為什麼採用時間作為優先順序屬性

這時候就是requestIdleCallback這個API的騷操作了, 這個API是幹嘛的呢?

window.requestIdleCallback()會在瀏覽器空閒時期依次呼叫函式, 這就可以讓開發者在主事件迴圈中執行後臺或低優先順序的任務,而且不會對像動畫和使用者互動這樣延遲觸發而且關鍵的事件產生影響。函式一般會按先進先呼叫的順序執行,除非函式在瀏覽器呼叫它之前就到了它的超時時間。

image

也就是說React實際上利用這個API在瀏覽器空閒期執行任務, 而這個API的回撥有個引數deadline , 當你超時的時候,無論是不是在空閒期都會執行該任務, 這也就解釋了為什麼React採用時間來做優先順序

不過實際上, React並沒有在版本中使用了這個API,而是通過requestAnimationFrame來hack,強行設定每一幀的到期時間為requestAnimationFrame回撥函式的引數加上33ms

var animationTick = function (rafTime) {
    isAnimationFrameScheduled = false;
    ...
    ...
    // 每幀到期時間為33ms
    frameDeadline = rafTime + 33
    if (!isIdleScheduled) {
      isIdleScheduled = true;
      window.postMessage(messageKey, '*');
    }
  };
複製程式碼

當然了, 分優先順序是有一個無法避免的問題, 那就是當有無數的優先順序更高的任務插進來, 就會形成飢餓現象,原有的任務會一直得不到機會執行

總結

React Fiber實際上就是一個任務調和器,它做到了將每一次更新切分成任務分片,從而擁有了可中斷且有優先順序的進行其他任務的功能。
在分析的過程中,發現了React的原始碼中使用了很多鏈式結構, 回撥鏈,任務鏈等,這個主要是為了增刪時效能比較高

相關文章