iT邦幫忙

1

React的秘密-原理解析第壹篇:核心概念

作為壹個構建用戶界面的庫,React的核心始終圍繞著更新這壹個重要的目標,將更新和極致的用戶體驗結合起來是React團隊壹直在努力的事情。為什麽React可以將用戶體驗做到這麽好?我想這是基於以下兩點原因:

  • Fiber架構和Scheduler出色的調度模式可以實現異步可中斷的更新行為。
  • 優先級機制貫穿更新的整個周期

本文是對React原理解讀系列的第1篇文章,後續的文章會定期更新,歡迎持續關註。在正式開始之前,我們先基於以上的這兩點展開介紹,以便對壹些概念可以先有個基礎認知。

配合的源碼調試環境在這裏 ,會跟隨React主要版本進行更新,歡迎隨意下載調試。

Fiber是什麽

Fiber是什麽?它是React的最小工作單元,在React的世界中,壹切都可以是組件。在普通的HTML頁面上,人為地將多個DOM元素整合在壹起可以組成壹個組件,HTML標簽可以是組件(HostComponent),普通的文本節點也可以是組件(HostText)。每壹個組件就對應著壹個fiber節點,許多個fiber節點互相嵌套、關聯,就組成了fiber樹,正如下面表示的Fiber樹和DOM的關系壹樣:

    Fiber樹                    DOM樹

   div#root                  div#root
      |                         |
    <App/>                     div
      |                       /   \
     div                     p     a
    /   ↖
   /      ↖
  p ----> <Child/>
             |
             a

壹個DOM節點壹定對應著壹個Fiber節點,但壹個Fiber節點卻不壹定有對應的DOM節點。

fiber 作為工作單元它的結構如下:

function FiberNode(
  tag: WorkTag,
  pendingProps: mixed,
  key: null | string,
  mode: TypeOfMode,
) {

  // Fiber元素的靜態屬性相關
  this.tag = tag;
  this.key = key; // fiber的key
  this.elementType = null;
  this.type = null; // fiber對應的DOM元素的標簽類型,div、p...
  this.stateNode = null; // fiber的實例,類組件場景下,是組件的類,HostComponent場景,是dom元素

  // Fiber 鏈表相關
  this.return = null; // 指向父級fiber
  this.child = null; // 指向子fiber
  this.sibling = null; // 同級兄弟fiber
  this.index = 0;

  this.ref = null; // ref相關

  // Fiber更新相關
  this.pendingProps = pendingProps;
  this.memoizedProps = null;
  this.updateQueue = null; // 存儲update的鏈表
  this.memoizedState = null; // 類組件存儲fiber的狀態,函數組件存儲hooks鏈表
  this.dependencies = null;

  this.mode = mode;

  // Effects
  // flags原為effectTag,表示當前這個fiber節點變化的類型:增、刪、改
  this.flags = NoFlags;
  this.nextEffect = null;

  // effect鏈相關,也就是那些需要更新的fiber節點
  this.firstEffect = null;
  this.lastEffect = null;

  this.lanes = NoLanes; // 該fiber中的優先級,它可以判斷當前節點是否需要更新
  this.childLanes = NoLanes;// 子樹中的優先級,它可以判斷當前節點的子樹是否需要更新

  /*
  * 可以看成是workInProgress(或current)樹中的和它壹樣的節點,
  * 可以通過這個字段是否為null判斷當前這個fiber處在更新還是創建過程
  * */
  this.alternate = null;

}

fiber架構下的React是如何更新的

首先要明白,React要完成壹次更新分為兩個階段: render階段和commit階段,兩個階段的工作可分別概括為新fiber樹的構建和更新最終效果的應用。

render階段

render階段實際上是在內存中構建壹棵新的fiber樹(稱為workInProgress樹),構建過程是依照現有fiber樹(current樹)從root開始深度優先遍歷再回溯到root的過程,這個過程中每個fiber節點都會經歷兩個階段:beginWork和completeWork。組件的狀態計算、diff的操作以及render函數的執行,發生在beginWork階段,effect鏈表的收集、被跳過的優先級的收集,發生在completeWork階段。構建workInProgress樹的過程中會有壹個workInProgress的指針記錄下當前構建到哪個fiber節點,這是React更新任務可恢復的重要原因之壹。

如下面的動圖,就是render階段的簡要過程:

fiberTask

commit階段

在render階段結束後,會進入commit階段,該階段不可中斷,主要是去依據workInProgress樹中有變化的那些節點(render階段的completeWork過程收集到的effect鏈表),去完成DOM操作,將更新應用到頁面上,除此之外,還會異步調度useEffect以及同步執行useLayoutEffect。

這兩個階段都是獨立的React任務,最後會進入Scheduler被調度。render階段采取的調度優先級是依據本次更新的優先級來決定的,以便高優先級任務的介入可以打斷低優先級任務的工作;commit階段的調度優先級采用的是最高優先級,以保證commit階段同步執行不可被打斷。

Scheduler 的作用

Scheduler用來調度執行上面提到的React任務。

何為調度?依據任務優先級來決定哪個任務先被執行。調度的目標是保證高優先級任務最先被執行。

何為執行?Scheduler執行任務具備壹個特點:即根據時間片去終止任務,並判斷任務是否完成,若未完成則繼續調用任務函數。它只是去做任務的中斷和恢復,而任務是否已經完成則要依賴React告訴它。Scheduler和React相互配合的模式可以讓React的任務執行具備異步可中斷的特點。

優先級機制

為了區分任務的輕重緩急,React內部有壹個從事件到調度的優先級機制。事件本身自帶優先級屬性,它導致的更新會基於事件的優先級計算出更新自己的優先級,更新會產生更新任務,更新任務的優先級由更新優先級計算而來,更新任務被調度,所以需要調度優先級去協調調度過程,調度優先級由更新任務優先級計算得出,就這樣壹步壹步,React將優先級的概念貫穿整個更新的生命周期。

React優先級相關的更多介紹請移步 React中的優先級

雙緩沖機制

雙緩沖機制是React管理更新工作的壹種手段,也是提升用戶體驗的重要機制。

當React開始更新工作之後,會有兩個fiber樹,壹個current樹,是當前顯示在頁面上內容對應的fiber樹。另壹個是workInProgress樹,它是依據current樹深度優先遍歷構建出來的新的fiber樹,所有的更新最終都會體現在workInProgress樹上。當更新未完成的時候,頁面上始終展示current樹對應的內容,當更新結束時(commit階段的最後),頁面內容對應的fiber樹會由current樹切換到workInProgress樹,此時workInProgress樹即成為新的current樹。

function commitRootImpl(root, renderPriorityLevel) {
    ...

    // finishedWork即為workInProgress樹的根節點,
    // root.current指向它來完成樹的切換
    root.current = finishedWork;

    ...
}

兩棵樹在進入commit階段時候的關系如下圖,最終commit階段完成時,兩棵樹會進行切換。
current樹和workInProgress樹

在未更新完成時依舊展示舊內容,保持交互,當更新完成立即切換到新內容,這樣可以做到新內容和舊內容無縫切換。

總結

本文基本概括了React大致的工作流程以及角色,本系列文章會以更新過程為主線,從render階段開始,壹直到commit階段,講解React工作的原理。除此之外,會對其他的重點內容進行大篇幅分析,如事件機制、Scheduler原理、重點Hooks以及context原理。

本系列文章耗時較長,落筆撰寫時,17版本還未發布,所以參照的源碼版本為16.13.1、17.0.0-alpha.0以及17共三個版本,我曾經對文章中涉及到的三個版本的代碼進行過核對,邏輯基本無差別,可放心閱讀。


尚未有邦友留言

立即登入留言