淺談React16框架 - Fiber



前言

React實現可以粗划為兩部分:reconciliation(diff階段)和 commit(操作DOM階段)。在 v16 之前,reconciliation 簡單說就是一個自頂向下遞歸算法,產出需要對當前DOM進行更新或替換的操作列表,一旦開始,會持續占用主線程,中斷操作卻不容易實現。當JS長時間執行(如大量計算等),會阻塞樣式計算、繪制等工作,出現頁面脫幀現象。所以,v16 進行了一次重寫,迎來了代號為Fiber的異步渲染架構。


Fiber

Fiber核心是實現了一個基於優先級和requestIdleCallback的循環任務調度算法。它包含以下特性:(參考:fiber-reconciler)

  • reconciliation階段可以把任務拆分成多個小任務
  • reconciliation階段可隨時中止或恢復任務
  • 可以根據優先級不同來選擇優先執行任務

從其特性可看出,Fiber核心是更換了reconciliation階段的運作。那么,問題來了:

  • 為什么對reconciliation階段進行拆分,commit階段呢?

    reconciliation階段包含的主要工作是對current tree 和 new tree 做diff計算,找出變化部分。進行遍歷、對比等是可以中斷,歇一會兒接着再來。
    commit階段是對上一階段獲取到的變化部分應用到真實的DOM樹中,是一系列的DOM操作。不僅要維護更復雜的DOM狀態,而且中斷后再繼續,會對用戶體驗造成影響。在普遍的應用場景下,此階段的耗時比diff計算等耗時相對短。
    所以,Fiber選擇在reconciliation階段拆分

  • 如何拆分呢?

首先,我們可以通過 A Cartoon Intro to Fiber中的一張圖來看:

  1. 用戶調用ReactDOM.render傳入組件,React創建Element樹;
  2. 在第一次渲染時,創建vdom樹,用來維護組件狀態和dom節點的信息(如List/Button/Item等)。當后續操作如render或setState時需要更新,通過diff算出變化的部分;
  3. 根據變化的部分更新vdom樹、調用組件生命周期函數等,同步應用到真實的DOM節點中。

在第二階段,Fiber是把render/update分片,拆解成多個小任務來執行,每次只檢查樹上部分節點,做完此部分后,若當前一幀(16ms)內還有足夠的時間就繼續做下一個小任務,時間不夠就停止操作,等主線程空閑時再恢復。
這種停止/恢復操作,需要記錄上下文信息。而當前只記錄單一dom節點的vDom tree 是無法完成的,
Fiber引入了fiber tree,是用來記錄上下文的vDom tree,可以理解為升級版的鋼鐵俠。

fiber tree上一個節點的結構大致有:

export type Fiber = {
    tag: TypeOfWork, // 類型
    type: 'div',
    return: Fiber|null, // 父節點
    child: Fiber|null, // 子節點
    sibling: Fiber|null, // 兄弟節點
    alternate: Fiber|null, //diff出的變化記錄在這個fiber上
    .....
};

完整樹結構可參見 ReactFiber

所以,Fiber是根據一個fiber節點(VDOM節點)來拆分,以fiber node為一個任務單元,一 個組件實例都是一個任務單元。任務循環中,每處理完一個fiber node,可以中斷/掛起/恢復。

  • 基於requestIdleCallback和優先級任務調度

1.requestIdleCallback

  window.requestIdleCallback(callback[, options])

瀏覽器提供的requestIdleCallback API中的Cooperative Scheduling可以讓瀏覽器在空閑時間執行回調(開發者傳入的方法),在回調參數中可以獲取到當前幀(16ms)剩余的時間。利用這個信息可以合理的安排當前幀需要做的事情,如果時間足夠,那繼續做下一個任務,如果時間不夠就歇一歇,調用requestIdleCallback來獲知主線程不忙的時候,再繼續做任務。

2.不同的任務分配不同的優先級,Fiber根據任務的優先級來動態調整任務調度,先做高優先級的任務

       {  
          NoWork: 0, // No work is pending.
          SynchronousPriority: 1, // 文本輸入框
          TaskPriority: 2, // 當前調度正執行的任務
          AnimationPriority: 3, // 動畫過渡
          HighPriority: 4, // 用戶交互反饋
          LowPriority: 5, // 數據的更新
          OffscreenPriority: 6, // 預估未來需要顯示的任務
      }

3.任務調度的過程是:

  1. 在任務隊列中選出高優先級的fiber node執行,調用requestIdleCallback獲取所剩時間,若執行時間超過了deathLine,或者突然插入更高優先級的任務,則執行中斷,保存當前結果,修改tag標記一下,設置為pending狀態,迅速收尾並再調用一個requestIdleCallback,等主線程釋放出來再繼續
  2. 恢復任務執行時,檢查tag是被中斷的任務,會接着繼續做任務或者重做

一個任務單元執行結束或掛起,會調用基於requestIdleCallback的調度器,返回一個新的任務隊列繼續進行上述過程。

  • 如何任務循環

    上面我們有介紹fiber node的屬性存放上下文信息的指向。

  • child:指向當前節點的子節點。當此節點任務結束后,如果child存在,就開始子節點的任務

  • sibling: 當子樹已遍歷到底部,回到父節點時,會拿到父節點的兄弟節點進行任務

  • return: 當當前節點沒有子節點時,會向上回到父節點

  • alternate
    在調用render或setState后,會克隆出一個鏡像fiber,diff產生出的變化會標記在鏡像fiber上。而alternate就是鏈接當前fiber tree和鏡像fiber tree, 用於斷點恢復。

    • workInProgress tree
      React中對其描述如下:

    work-in-progress
    A fiber that has not yet completed; conceptually, a stack frame which has not yet returned.The alternate of the current fiber is the work-in-progress, and the alternate of the work-in-progress is the current fiber.

    當前fiber節點的alternate屬性指向workInProgress節點,對應workInProgress節點的alternate屬性指向當前fiber節點。
    

    上面alternate中說到鏡像fiber tree就是workInProgress tree。
    workInProgress tree上每個節點都有一個effect list,用來存放需要更新的內容。此節點更新完畢會向子節點或鄰近節點合並 effect list。

    任務循環的過程如下:

    1. 找到高優先級的待處理的節點
    2. 檢查當前節點是否需要更新,不需要的話,直接clone子節點,直接到5
    3. 打個tag標記,更新自己(組件更新props,context等,DOM節點記下DOM change)
    4. 通過render獲取子節點,生成子節點的workInProgress節點
    5. 若沒有產生子節點,歸並diff出的不同部分effect list(包含DOM change)到父節點
    6. 把子節點或兄弟節點作為待處理任務,准備進入下一個任務循環。若已經回到了workInProgress tree的根節點,則任務循環結束

    通過每個節點更新結束時向上歸並effect list來收集任務結果,reconciliation結束后,根節點的effect list里記錄了包括DOM change在內的所有side effect

    通過一段代碼,我們來看看如何進行任務循環:

    export class Home extend React.component<HomeProps, any> {
        componentWillReceiveProps(nextProps: HomeProps) {}
        componentDidMount() {}
        componentDidUpdate() {}
        componentWillUnmount() {}
        .....
        render() {
            return (
                <div className="top">
                    <span>ZZ</span>
                    <button>click</button>
                </div>
            )
        }
    }
    ReactDom.render(<Home />, document.querySelector(selectors: '#hostRoot'))
    

    以它為例創建fiber tree:

  1. 從根節點 #hostRoot出發,走到它的child節點 div.top
  2. div.top有兩個子節點span和button ,先從左子樹span開始
  3. span只有一個子節點 ZZ,到達樹的底部后原路返回
  4. 返回到span時發現其有一個兄弟節點 button, 走去button
  5. button有一個子節點 click, 同樣到達樹的底部后原路返回到button
  6. button的右邊沒有兄弟了(若有,則繼續重復4-5),返回到父節點 div.top
  7. 最后回到根節點 #hostRoot

再次setState或render時,構建workInProgress tree:

  1. 先把根節點 #hostRoot克隆出來,並用child屬性指向它在fiber tree中的子節點div.top
  2. 把div.top從fiber tree中克隆出來,需要更新的話,加一個tag標志
  3. 更新div.top節點的狀態,屬性的指向等,並通過render獲取到它的新子節點
  4. 盡量復用舊子節點span或button來創建新子節點的workInProgress結構
  5. 把新節點button做為下一個任務,調用requestIdleCallback獲取當前幀所剩余的時間,如果還足夠,就繼續button的任務,重復2-4,否則,就等主線程空閑后再開始循環
  6. 當子節點submit中沒有child屬性的指向,表示已到達底部。會把diff時產生的effect list會merge回return屬性指向的父節點button上
  7. 當父節點button沒有兄弟節點時,會一直向上return回根節點,並把每個節點上產生的effect-list合並到根節點上。任務循環結束

我們再來看下源碼中的reconcilation階段:

  • workLoop階段

首先獲取到下一個任務的執行優先級,調用一個performUnitOfWork函數進入reconcilation階段的工作, 分為beginWork和completeWork

1、beginWork

beginWork中只是處理節點的不同tag屬性對應不同的更新方法。如 updateClassComponent方法對應的是tag類型是React組件實例。

reconcileChildren 實現對新老節點的diff。其內部主要是調用 reconcileChildFibers 方法。

React中diff是基於同層級節點的,節點的移動/插入/刪除等操作都是在fiber tree的同一層級中進行。

2、 completeWork
completeWork的工作主要是通過新老節點的prop或tag等,收集節點的effect-list。然后向上一層一層循環,merge每個節點的effect-list,當到達根節點#hostRoot時,節點上包含所有的effect-list。並把effect-list傳給pendingcommit,進入commit階段。


  • Fiber對生命周期的影響
  // reconciliation階段
  componentWillMount
  componentWillReceiveProps
  shouldComponentUpdate
  componentWillUpdate
  --------------------------------
  // commit階段
  componentDidMount
  componentDidUpdate
  componentWillUnmount

在我們之前的文章中也介紹過,在reconciliation階段,生命周期函數會被多次調用,開發者慎重調用這個階段的生命周期。
Reactv16.3已經開始了廢棄計划:

  • 16.3版本:引入帶UNSAFE_前綴的3個生命周期函數UNSAFE_componentWillMount,UNSAFE_componentWillReceiveProps和UNSAFE_componentWillUpdate,這個階段新舊6個函數都能用。新引入生命周期函數getDerivedStateFromProps和getSnapshotBeforeUpdate,用來代替componentWillReceiveProps和componentWillUpdate
  • 16.3+版本:警告componentWillMount,componentWillReceiveProps和componentWillUpdate即將過時,這個階段新舊6個函數也都能用,只是舊的在DEV環境會報Warning
  • 17.0版本:正式廢棄componentWillMount,componentWillReceiveProps和componentWillUpdate,這個階段只有新的帶UNSAFE_前綴的3個函數能用,舊的不會再觸發

本文只是分析了reconciliation階段,不涉及commit階段,感興趣的同學可以研究下。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM