從中斷機制看 React Fiber 技術

前言—

React 16 開始,採用了 Fiber 機制替代了原有的同步渲染 VDOM 的方案,提高了頁面渲染性能和用戶體驗。Fiber 究竟是什麼,網上也很多優秀的技術揭祕文章,本篇主要想從計算機的中斷機制來聊聊 React Fiber 技術大概工作原理。

單任務—

在早期的單任務系統上,用戶一次只能提交一個任務,當前運行的任務擁有全部硬件和軟件資源,如果任務不主動釋放 CPU 控制權,那麼將一直佔用所有資源,可能影響其他任務,造成資源浪費。該模式非常像當前瀏覽器運行模式,由於 UI 線程和 JS 線程的運行是互斥的,一旦 JS 長時間執行,瀏覽器無法及時響應用戶交互,很容造成界面的卡頓,React 早期的同步渲染機制,當一次性更新的節點太多時,影響用戶體驗。

中斷—

中斷最初是用於提高處理器效率的一種手段,在沒有中斷的情況下,當 CPU 在執行一段代碼時,如果程序不主動退出(如:一段無限循環代碼),那麼 CPU 將被一直佔用,影響其他任務運行。

while(true) {
  ...
};

而中斷機制會強制中斷當前 CPU 所執行的代碼,轉而去執行先前註冊好的中斷服務程序。比較常見的如:時鐘中斷,它每隔一定時間將中斷當前正在執行的任務,並立刻執行預先設置的中斷服務程序,從而實現不同任務之間的交替執行,這也是在多任務系統的重要的基礎機制。中斷機制主要通過硬件觸發,CPU 屬於被動接受。有了中斷後,各任務執行時間就可以得到非常好的控制。

回到瀏覽器,目前瀏覽器大多是 60Hz(60 幀 / 秒),既每一幀耗時大概在 16ms 左右,它會經過下面這幾個過程:

  1. 輸入事件處理

  2. requestAnimationFrame

  3. DOM 渲染

  4. RIC (RequestIdleCallback)

我們除了在步驟 1-3 的中進行加塞外,無法進行任何干預,而步驟 4 的 RIC,算是一種防止多餘計算資源被浪費的機制,例如,當一幀中步驟 1-3 只耗費 6ms,那麼剩餘 10ms 的計算資源則會被浪費,而 RIC 就是瀏覽器提供的一種資源利用的接口。RIC 非常像前面提到的 “中斷服務”,而瀏覽器的每一幀類似 “中斷機制”,利用它則可以在實現我們前面提到的大任務卡頓問題,例如:之前我們在 JS 中寫如下代碼時,無疑會阻塞瀏覽器渲染。

function task(){
  while(true){
   ...
  };
}
task();

但利用 RIC 機制後,我們完全可以讓大任務週期性的執行,從而不阻止瀏覽器正常渲染。

將上面示例代碼根據 RequestIdleCallback 進行調整,如下:

function task(){
  while(true){
   ...
  };
}
requestIdleCallback(task);

遺憾的是,由於我們的代碼運行在用戶態,無法感知到底層的真實中斷,我們現在利用的 RIC 也只是一種中斷的近似模擬,以上代碼並不會在 16ms 到期後被強制中斷,我們只能主動進行釋放,將控制權交還瀏覽器,RIC 提供了 timeRemaining 方法,讓任務知道主動釋放時機,我們調整以上代碼,如下:

function task(deadline){
  while(true){
   ...
   if(!deadline.timeRemaining()) {
     requestIdleCallback(task);
     // 主動退出循環,將控制權交還瀏覽器
     break;
   }
  };
}
requestIdleCallback(task);

以上示例,可以讓一個大循環在 “中斷” 機制下,不阻塞瀏覽器的渲染和響應。

注意: RIC 調用頻率大概是 20 次 / 秒,遠遠低於頁面流暢度的要求!這樣每次你能得到差不多 50ms 的計算時間,如果完全用這 50ms 來做計算,同樣會帶來交互上的卡頓,所以 React Fiber 是基於自定義一套機制來模擬實現。

例如:setTimeout、setImmediate、MessageChannel。

以下是 React Fiber 中的主動釋放片段代碼:

function workLoop(hasTimeRemaining, initialTime) {
  let currentTime = initialTime;
  advanceTimers(currentTime);
  currentTask = peek(taskQueue);
  while (
    currentTask !== null &&
    !(enableSchedulerDebugging && isSchedulerPaused)
  ) {
    if (
      currentTask.expirationTime > currentTime &&
      (!hasTimeRemaining || shouldYieldToHost())
    ) {
      // 如果超時,則主動退出循環,將控制權交還瀏覽器
      break;
    }
    ...
  }
  ...
}

調度任務—

有了中斷機制,中斷服務後,不同任務就能實現間斷執行的可能,如何實現多任務的合理調度,就需要一個調度任務來進行處理,這通常代表着操作系統。例如,當一個任務 A 在執行到一半時,被中斷機制強制中斷,此時操作系統需要對當前任務 A 進行現場保護,如:寄存器數據,然後切換到下一個任務 B,當任務 A 再次被調度時,操作系統需要還原之前任務 A 的現場信息,如:寄存器數據,從而保證任務 A 能繼續執行下一半任務。調度過程中如何保證被中斷任務的信息不被破壞是一個非常重要的功能。

瀏覽器提供的 RIC 機制,類似 “中斷服務” 註冊機制,註冊後我們只要合適的時機進行釋放,就能實現 “中斷” 效果,剛也提到對於不同任務之間切換,在中斷後,需要考慮現場保護和現場還原。早期 React 是同步渲染機制,實際上是一個遞歸過程,遞歸可能會帶來長的調用棧,這其實會給現場保護和還原變得複雜,React Fiber 的做法將遞歸過程拆分成一系列小任務(Fiber),轉換成線性的鏈表結構,此時現場保護只需要保存下一個任務結構信息即可,所以拆分的任務上需要擴展額外信息,該結構記錄着任務執行時所需要的必備信息:

{
    stateNode,
    child,
    return,
    sibling,
    expirationTime
    ...
}

我們看以下示例代碼:

ReactDOM.render(
  <div id="A">
    A
    <div id="B">
      B<div id="C">C</div>
    </div>
    <div id="D">D</div>
  </div>,
  node
);

當 React 進行渲染時,會生成如下任務鏈,此時如果在執行任務 B 後時發現時間不足,主動釋放後,只需要記錄下一次任務 C 的信息,等再次調度時取得上次記錄的信息即可。使用該機制後,對於渲染任務的優先級、撤銷、掛起、恢復都能得到非常好的控制。

總結—

中斷機制其實是一種非常重要的解決資源共享的手段,對於操作系統而言,它已經是一個必不可少功能。隨着瀏覽器的功能越來越強,越來越多功能也搬到了瀏覽器,如何保證用戶在使用過程中的流暢,也是經常需要思考的問題,在業務開發過程中,我們可以根據實際場景利用好 “中斷機制”,提高用戶體驗。

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/7bq7JuSZIkn8u_Z36COZAQ