深入理解 Render 階段 Fiber 樹的初始化與更新

作者:抱枕同學

https://juejin.cn/post/7202085514400038969

一、前言

爲什麼有這篇文章?當時有人問我下面這個點擊button,網頁應該變成什麼樣? 注意他們的key是相同的

import React, { useState } from "react";

function Demo2() {
  const [count, setCount] = useState(0);

  return (
    <div>
      <button onClick={() => setCount((i) => i + 1)}>點擊Count+1</button>
      <h3 key={count}>大{count}</h3>
      <h2 key={count}>舌{count}</h2>
      <h1 key={count}>頭{count}</h1>
    </div>
  );
}

export default Demo2;

答案和你想象的一樣嗎??不一樣就繼續往下看看唄!!!結尾有答案

二、前置概念

react 框架可以用來表示,輸入狀態 —> 吐出 ui。

const ui = fn(state)

react 架構是什麼?

可以分爲如下三層:

  1. scheduler(調度器):用來分發優先級更高的任務。

  2. render 階段(協調器):找出哪些節點發生了變化,並且給相應的 fiber 打上標籤。

  3. commit 階段(渲染器):將打好標籤的節點渲染到視圖上。遍歷 effectList 執行對應的 dom 操作或部分生命週期

  1. 輸入: 將每一次更新 (如: 新增, 刪除, 修改節點之後) 視爲一次更新需求(目的是要更新 DOM 節點).

  2. 註冊調度任務: react-reconciler 收到更新需求之後, 並不會立即構造 fiber 樹, 而是去調度中心 scheduler 註冊一個新任務 task, 即把更新需求轉換成一個 task.

  3. 執行調度任務 (輸出): 調度中心 scheduler 通過任務調度循環來執行 task

  4. fiber 構造循環是 task 的實現環節之一, 循環完成之後會構造出最新的 fiber 樹.

  5. commitRoot 是 task 的實現環節之二, 把最新的 fiber 樹最終渲染到頁面上, task 完成.

主幹邏輯就是輸入到輸出這一條鏈路, 爲了更好的性能 (如批量更新, 可中斷渲染等功能), react 在輸入到輸出的鏈路上做了很多優化策略, 任務調度循環和 fiber 構造循環相互配合就可以實現可中斷渲染.

ReactElement, Fiber, DOM 三者的關係

上面我們大概提及了一下 react 的架構和更新的粗略流程,考慮到本文的重點是 Render 階段發生了啥,接下來上重量級嘉賓 JSX,ReactElement, Fiber, DOM。以下面這個 jsx 代碼爲例,講解三者的關係

function Test() {
  const [showName, setShowName] = useState(true);
  return (
    <div>
      <div>今天肯德基瘋狂星期八,和我一起玩彩虹六?</div>
      <ul>
        <li>抱枕一號</li>
        {showName && <li>抱枕二號</li>}
      </ul>
      <div
        onClick={() ={
          setShowName(false);
        }}
      >
        點擊讓高啓強少一個小弟
      </div>
    </div>
  );
}

createElement源碼

所有采用JSX語法書寫的節點, 都會被編譯器轉換, 最終會以React.createElement(...)的方式, 創建出來一個與之對應的ReactElement對象.

這也是爲什麼在每個使用JSX的 JS 文件中,你必須顯式的聲明 import React from 'react';(17 版本後不需要)否則在運行時該模塊內就會報未定義變量 React 的錯誤。

ReactElement 數據結構和內存結構(結合上面 jsx 示例代碼)

數據結構
export type ReactElement = {
  // 用於辨別ReactElement對象形式
  $$typeof: any,

  // 內部屬性
  type: any, // 表明其種類
  key: any,
  ref: any,
  props: any,

  // ReactFiber 記錄創建本對象的Fiber節點, 還未與Fiber樹關聯之前, 該屬性爲null
  _owner: any,

  // __DEV__ dev環境下的一些額外信息, 如文件路徑, 文件名, 行列信息等
  _store: {validated: boolean, ...},
  _self: React$Element<any>,
  _shadowChildren: any,
  _source: Source,
};
內存結構

Fiber 對象數據結構

數據結構
export type Fiber = {|
  tag: WorkTag,
  key: null | string, // 和ReactElement組件的 key 一致.
  elementType: any,//一般來講和ReactElement組件的 type 一致 比如div ul
  type: any, // 一般來講和fiber.elementType一致. 一些特殊情形下, 比如在開發環境下爲了兼容熱更新
  stateNode: any, // 真實DOM是誰
  return: Fiber | null, //爹是誰
  child: Fiber | null, //孩子是誰
  sibling: Fiber | null, //兄弟是誰
  index: number, 
  ref:
    | null
    | (((handle: mixed) => void) & { _stringRef: ?string, ... })
    | RefObject, //指向在ReactElement組件上設置的 ref
  pendingProps: any, // 從`ReactElement`對象傳入的 props. 用於和`fiber.memoizedProps`比較可以得出屬性是否變動
  memoizedProps: any, // 上一次生成子節點時用到的屬性, 生成子節點之後保持在內存中
  updateQueue: mixed, // 存儲state更新的隊列, 當前節點的state改動之後, 都會創建一個update對象添加到這個隊列中.
  memoizedState: any, // 用於輸出的state, 最終渲染所使用的state
  dependencies: Dependencies | null, // 該fiber節點所依賴的(contexts, events)等
  mode: TypeOfMode, // 二進制位Bitfield,繼承至父節點,影響本fiber節點及其子樹中所有節點. 與react應用的運行模式有關(有ConcurrentMode, BlockingMode, NoMode等選項).

  // 優先級相關
  lanes: Lanes, // 本fiber節點的優先級
  childLanes: Lanes, // 子節點的優先級
  alternate: Fiber | null, // 雙fiber緩存 指向內存中的另一個fiber, 每個被更新過fiber節點在內存中都是成對出現(current和workInProgress)
|};
內存結構

ReactElement, Fiber, DOM 三者的關係

React 的啓動過程發生了啥

接下來介紹的都是當前穩定版legacy 模式

ReactDOM.render(<App />, document.getElementById('root')dom ={});

在沒有進入render階段(react-reconciler包)之前,reactElement(<App/>)和 DOM 對象div#root之間沒有關聯。

在 react 初始化的時候,會創建三個全局對象,在三個對象創建完畢的時候,react 初始化完畢。

  1. ReactDOMRoot對象

  2. 屬於react-dom包,該對象暴露有 render,unmount 方法, 通過調用該實例的ReactDOM.render方法, 可以引導 react 應用的啓動.

  3. fiberRoot對象

  4. 屬於react-reconciler包, 在運行過程中的全局上下文, 保存 fiber 構建過程中所依賴的全局狀態,

  5. 其大部分實例變量用來存儲fiber構造循環過程的各種狀態,react 應用內部, 可以根據這些實例變量的值, 控制執行邏輯。

  6. HostRootFiber對象

  7. 屬於react-reconciler包,這是 react 應用中的第一個 Fiber 對象, 是 Fiber 樹的根節點, 節點的類型是HostRoot.

這 3 個對象是 react 體系得以運行的基本保障, 除非卸載整個應用,否則不會再銷燬

此刻內存中各個對象的引用情況表示出來,此時reactElement(<App/>)還是獨立在外的, 還沒有和目前創建的 3 個全局對象關聯起來

到此爲止, react內部經過一系列運轉, 完成了初始化。

三、render 階段發生了啥

以下所有示例按照下面的代碼 請注意

class App extends React.Component {
state = {
  list: ['A''B''C'],
};
onChange = () ={
   this.setState({ list: ['C''A''X'] });
};
componentDidMount() {
  console.log(`App Mount`);
}
render() {
  return (
    <>
      <Header key='d' />
      <button key='e'>change</button>
      <div class key='f'>
      {this.state.list.map(item =(
         <p key={item}>{item}</p>
       ))}
      </div>
    </>
  );
 }
}

class Header extends React.PureComponent {
render() {
   return (
   <>
    <h1>title</h1>
    <h2>title2</h2>
   </>
  );
 }
}

雙緩衝 fiber 技術

在上文我們梳理了ReactElement, Fiber, DOM三者的關係, fiber樹的構造過程, 就是把ReactElement轉換成fiber樹的過程. 但是在這個過程中, 內存裏會同時存在 2 棵fiber樹:

React 入口初始化內存情況

在進入react-reconciler包之前, 也就是還沒render時, 內存狀態圖如下,和上面啓動過程的圖對應:

fiber 樹構造方式

  1. 初次創建: 在React應用首次啓動時, 界面還沒有渲染, 此時並不會進入對比過程, 相當於直接構造一棵全新的樹.

  2. 對比更新: React應用啓動後, 界面已經渲染. 如果再次發生更新, 創建新fiber之前需要和舊fiber進行對比. 最後構造的 fiber 樹有可能是全新的, 也可能是部分更新的.

在深度優先遍歷中, 每個fiber節點都會經歷 2 個階段:

  1. 探尋階段 beginWork

  2. 回溯階段 completeWork

beginWork探尋階段發生了什麼源碼地址 [1]

  1. 創建節點:根據 ReactElement對象創建所有的fiber節點, 最終構造出 fiber 樹形結構 (設置returnsibling指針)

  2. 給節點打標籤:設置fiber.flags(二進制形式變量, 用來標記 fiber 節點 的增, 刪, 改狀態, 等待completeWork階段處理)

  3. 設置真實 DOM 的局部狀態:設置fiber.stateNode局部狀態 (如 Class 類型節點: fiber.stateNode=new Class())

completeWork回溯階段發生了什麼源碼地址 [2]

  1. 調用completeWork

  2. fiber節點 (tag=HostComponent, HostText) 創建 DOM 實例, 設置fiber.stateNode局部狀態 (如tag=HostComponent, HostText節點: fiber.stateNode 指向這個 DOM 實例).

  3. 爲 DOM 節點設置屬性, 綁定事件 (合成事件原理).

  4. 設置fiber.flags標記

  5. 把當前 fiber 對象的副作用隊列 (firstEffectlastEffect) 添加到父節點的副作用隊列之後, 更新父節點的firstEffectlastEffect指針.

  6. 識別beginWork階段設置的fiber.flags, 判斷當前 fiber 是否有副作用 (增, 刪, 改), 如果有, 需要將當前 fiber 加入到父節點的effects隊列, 等待commit階段處理.

初次創建

這有一個動畫,具體如果想看流程圖可以點擊 https://yzcoyzhsws.feishu.cn/docx/IB0gdVMjAo1TDdxLDXwcx7oan9c

下面標註了生成時期的 beginWorkcompleteWork 執行過程

  // 將最新的fiber樹掛載到root.finishedWork節點上 下面綠色粗線表示指針
  const finishedWork: Fiber = (root.current.alternate: any);
  root.finishedWork = finishedWork;
  root.finishedLanes = lanes;
  // 進入commit階段
  commitRoot(root);

動畫演示了初次創建fiber樹的全部過程, 跟蹤了創建過程中內存引用的變化情況. fiber樹構造循環負責構造新的fiber樹, 構造過程中同時標記fiber.flags, 最終把所有被標記的fiber節點收集到一個副作用隊列中, 這個副作用隊列被掛載到根節點上 (HostRootFiber.alternate.firstEffect). 此時的fiber樹和與之對應的DOM節點都還在內存當中, 等待commitRoot階段進行渲染

對比更新的時候發生了什麼

1. 優化原則
  1. 只對同級節點進行對比,如果 DOM 節點跨層級移動,則 react 不會複用
  1. 不同類型的元素會產出不同的結構,會銷燬老的結構,創建新的結構

  2. 可以通過 key 標示移動的元素

  3. 類型一致的節點纔有繼續 diff 的必要性

diff 算法介紹
  1. 單節點

  2. 如果是新增節點, 直接新建 fiber, 沒有多餘的邏輯

  3. 如果是對比更新

單節點的邏輯比較簡明, 源碼 [3]

  1. 多節點

  2. 多節點一般會存在兩輪遍歷,第一輪尋找公共序列,第二輪遍歷剩餘非公共序列

  3. 第一次循環 源碼 [4]

  1. 如果newChildren遍歷完(即i === newChildren.length - 1)或者oldFiber遍歷完(即oldFiber.sibling === null),跳出遍歷,第一輪遍歷結束。

  2. let i = 0,遍歷newChildren,將newChildren[i]oldFiber比較,判斷DOM節點是否可複用。

  3. 如果可複用,i++,繼續比較newChildren[i]oldFiber.sibling,可以複用則繼續遍歷。

  4. 如果不可複用,分兩種情況:

  1. 第二次循環: 遍歷剩餘非公共序列, 優先複用 oldFiber 序列中的節點。

下面動畫展示了 fiber 的對比更新過程 每一張流程圖鏈接 [6]

四、檢驗學習成果

爲什麼網頁會變成那個樣子?

import React, { useState } from "react";

function Demo2() {
  const [count, setCount] = useState(0);

  return (
    <div>
      <button onClick={() => setCount((i) => i + 1)}>點擊Count+1</button>
      <h3 key={count}>大{count}</h3>
      <h2 key={count}>舌{count}</h2>
      <h1 key={count}>頭{count}</h1>
    </div>
  );
}

export default Demo2;

五、參考

1、7km:https://7kms.github.io/react-illustration-series/main/macro-structure%EF%BC%88%E5%BC%BA%E7%83%88%E6%8E%A8%E8%8D%90%EF%BC%89

2、冴羽:https://juejin.cn/post/7160981608885927972

3、卡頌:https://react.iamkasong.com/preparation/idea.html

4、https://xiaochen1024.com/article_item/600ac4384bf83f002edaf54a

參考資料

[1]

https://github.com/facebook/react/blob/v17.0.2/packages/react-reconciler/src/ReactFiberBeginWork.old.js#L3083-L3494: https://link.juejin.cn?target=https%3A%2F%2Fgithub.com%2Ffacebook%2Freact%2Fblob%2Fv17.0.2%2Fpackages%2Freact-reconciler%2Fsrc%2FReactFiberBeginWork.old.js%23L3083-L3494

[2]

https://github.com/facebook/react/blob/v17.0.2/packages/react-reconciler/src/ReactFiberWorkLoop.old.js#L1670-L1802: https://link.juejin.cn?target=https%3A%2F%2Fgithub.com%2Ffacebook%2Freact%2Fblob%2Fv17.0.2%2Fpackages%2Freact-reconciler%2Fsrc%2FReactFiberWorkLoop.old.js%23L1670-L1802

[3]

源碼: https://github.com/facebook/react/blob/v17.0.2/packages/react-reconciler/src/ReactChildFiber.old.js#L1135-L1233

[4]

源碼: https://github.com/facebook/react/blob/1fb18e22ae66fdb1dc127347e169e73948778e5a/packages/react-reconciler/src/ReactChildFiber.new.js#L818

[5]

源碼: https://github.com/facebook/react/blob/1fb18e22ae66fdb1dc127347e169e73948778e5a/packages/react-reconciler/src/ReactChildFiber.new.js#L893

[6]

https://yzcoyzhsws.feishu.cn/docx/NLDDdzRPLo9qU8x7ek2clIN6nWc: https://yzcoyzhsws.feishu.cn/docx/NLDDdzRPLo9qU8x7ek2clIN6nWc

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