【精通react】(一)react函数式组件,是如何转换成真实dom的

227 阅读7分钟

React 函数组件转换为真实 DOM 的过程可以分为以下几个关键步骤:

graph LR
    A[JSX] --> B[Babel 编译]
    B --> C[React 元素对象]
    C --> D[虚拟 DOM 树]
    D --> E[Fiber Tree]
    E --> F[Diff 算法]
    F --> G[副作用标记]
    G --> H[Commit 阶段]
    H --> I[真实 DOM]

一. JSX 转换为 React 元素对象

函数组件中返回的 JSX 代码会被 Babel 编译器 转换为 React.createElement 调用,最终生成一个 React 元素对象(JS 对象)

function MyComponent() {
  return <div>Hello World</div>;
}

编译后:

function MyComponent() {
  return React.createElement("div", null, "Hello World");
}

生成的 React 元素对象结构类似:

{
  $$typeof: Symbol(react.element),
  type: 'div', // 标签类型
  key: null,
  ref: null,
  props: {
    children: 'Hello World'
  },
  _owner: null,
  ...
}

二. 生成虚拟 DOM 树

当调用 ReactDOM.render() 或 react-dom/client 的 createRoot().render() 时,React 会执行以下操作:

  • 调用函数组件:执行 MyComponent(),得到一个 React 元素对象(虚拟 DOM)。
  • 递归构建虚拟 DOM 树:如果组件中包含嵌套的子组件,React 会递归调用所有子组件函数,生成完整的虚拟 DOM 树。

三. 虚拟 DOM 转换为 fiber tree

虚拟 DOM(Virtual DOM)转换为 Fiber Tree 是 协调阶段(Reconciliation Phase)  的核心步骤之一。这一过程将组件树的虚拟表示(虚拟 DOM)转化为 React 内部的链表结构(Fiber Tree),以便支持 可中断的渲染 和 优先级调度。每个虚拟 DOM 节点在 Fiber 架构中被映射为一个 Fiber 节点,并附加了状态和调度信息。

1. 虚拟 DOM 节点

[1]. 定义

虚拟 DOM 是 React 用来描述 UI 状态的一种 内存中的 JavaScript 对象树。它是一个轻量级的、与真实 DOM 类似的结构,用于高效比较和更新真实 DOM。

[2]. 作用
  • 构建 UI 的快照:每次组件渲染时,React 会生成一个新的虚拟 DOM 树(如果组件包裹了memo,则根据props是否改变决定是否复用旧的vdom节点)。
[3]. 特点
  • 独立于渲染引擎:虚拟 DOM 本身不直接操作真实 DOM。
  • 递归结构:每个组件返回的 JSX 最终都会被转换为虚拟 DOM 节点(ReactElement 对象)。

虚拟 DOM 是由 ReactElement 对象组成的树,例如:

{
  type: "div",
  props: { children: "Hello" },
  key: null,
  ref: null
}

2. Fiber 节点

[1]. 定义

Fiber 是 React 16 引入的 新协调引擎(Reconciliation Engine) ,用于改进渲染性能和用户体验。它将原本递归式的渲染流程拆分为可中断、可恢复的单元任务。

[2]. 作用
  • 支持并发渲染:允许浏览器在渲染过程中处理高优先级任务(如用户输入)。
  • 增量渲染:将渲染工作切分为小块,利用浏览器的空闲时间逐步完成。
  • 优先级调度:根据任务类型(如动画、输入、渲染)分配不同的优先级。
[3]. 特点
  • 非递归结构:Fiber 使用链表结构替代递归,便于任务中断和恢复。
  • 每个虚拟 DOM 节点对应一个 Fiber 节点:Fiber 节点包含额外的状态信息(如副作用链表、优先级等)。
[4]. Fiber 节点结构示例
{
  // 基本属性
  type: "div",
  props: { children: "Hello" },
  // Fiber 特有属性
  child: null,      // 指向子 Fiber 节点
  sibling: null,    // 指向兄弟 Fiber 节点
  return: parentFiber, // 指向父 Fiber 节点
  stateNode: domNode,  // 对应的真实 DOM 节点
  pendingProps: {},     // 待更新的 props
  memoizedProps: {},    // 上一次渲染的 props
  updateQueue: null,    // 更新队列
  effectTag: "Placement", // 副作用标记(如插入、更新、删除)
  nextEffect: null,     // 指向下一个带副作用的 Fiber 节点
  ...
}

3.vdom 与 fiber的区别

特性虚拟 DOM(VDOM)Fiber
本质数据结构(描述 UI 的 JavaScript 对象树)算法(协调引擎,负责渲染调度)
作用快速构建 UI 快照,用于 Diff 算法实现并发渲染、任务拆分、优先级调度
生命周期每次组件渲染时生成仅在 React 协调阶段初始化一次
是否可中断不可中断(递归结构)可中断(链表结构)
是否依赖关系Fiber 架构需要虚拟 DOM 作为输入虚拟 DOM 独立于 Fiber 架构

4、vdome向fiber tree的转换过程

[1]. 深度优先遍历虚拟 DOM
  • React 以 深度优先 的方式遍历虚拟 DOM 树。

  • 每个节点被转换为一个 Fiber 节点,并通过链表指针连接:

    • child:指向第一个子节点。
    • sibling:指向下一个兄弟节点。
    • return:指向上层父节点。
[2]. Fiber 节点的创建
  • 函数式组件的Fiber 节点的创建

    • 创建 FunctionComponent 类型的 Fiber 节点。
    • 附加 Hook 信息(如 useStateuseEffect 的状态)。
  • 原生 DOM 节点的Fiber 节点的创建(如 <div><span>):

    • 创建 HostComponent 类型的 Fiber 节点。
    • 附加 DOM 相关属性(如 tagtype)。
[3]. 链表结构的构建
  • 虚拟 DOM 的递归结构被转换为 链表结构,便于中断和恢复。

  • 示例
    虚拟 DOM 的嵌套结构:

    <div>
      <p>Hello</p>
    </div>
    

    对应的 Fiber Tree 链表结构:

    {
      type: "div",
      child: {
        type: "p",
        sibling: null,
        return: divFiber
      },
      return: null
    }
    

[4]、Fiber Tree 的核心作用
  1. 支持可中断的渲染

    • 链表结构允许 React 在遍历 Fiber Tree 时检查是否需要中断(如响应用户输入),并通过 shouldYield() 判断。
  2. 优先级调度

    • 每个 Fiber 节点可以分配不同的优先级(如动画 > 用户输入 > 渲染),高优先级任务可抢占低优先级任务。
  3. 副作用标记

    • 在转换过程中,React 会标记 Fiber 节点的副作用(如 Placement 插入、Update 更新、Deletion 删除),供提交阶段使用。
    • 在更新过程中,通过diff算法,比较新旧两棵fiber tree,将对应节点标记副作用(如 Placement 插入、Update 更新、Deletion 删除)。注:diff算法应用在新旧fiber树的比较,而不是新旧vdom的比较。

四. commit 阶段

在 React 中,提交阶段(Commit Phase)  是渲染流程的最后一步,负责将 协调阶段(Reconciliation Phase)  标记的副作用(如插入、更新、删除 DOM 节点)应用到真实 DOM 上。

1、提交阶段的核心任务

[1]. 应用副作用(Effect Tags)
-   根据协调阶段标记的副作用(如 `Placement``Update``Deletion`),操作真实 DOM。

-   **示例**    -   `Placement`:将新节点插入 DOM。
    -   `Update`:更新已有节点的属性(如 `textContent``className`)。
    -   `Deletion`:从 DOM 中移除旧节点。
[2]. 执行 useEffect 副作用
-   在 DOM 更新完成后,依次执行所有 `useEffect` 回调函数。
-   **注意**`useEffect` 的执行时机是 **提交阶段的最后一步**,确保副作用基于最新的 DOM 状态。
[3]. 处理 ref 的挂载与卸载
-   挂载 `ref`:当节点插入 DOM 时,调用 `ref.callback(currentValue)`-   卸载 `ref`:当节点从 DOM 移除时,调用 `ref.callback(null)`

2、Commit 阶段的批量更新机制

函数式组件在 Commit 阶段的批量更新 是通过 副作用链表(Side Effect List)  和 DOM 操作的批量合并 实现的。React 会确保在 Commit 阶段一次性完成所有 DOM 操作和副作用的执行,从而减少浏览器的重排(Reflow)和重绘(Repaint),提高性能。

  1. 副作用链表(Side Effect List)

    • 在 协调阶段(Reconciliation Phase) ,React 会为每个需要操作的 Fiber 节点标记副作用(如 PlacementUpdateDeletion)。
    • 这些节点通过 nextEffect 指针串联成一个 副作用链表,React 在 Commit 阶段会按顺序遍历该链表,依次处理每个节点的副作用。
  2. DOM 操作的批量合并

    • React 会 批量执行所有 DOM 操作(如插入、更新、删除),而不是逐个节点操作。

    • 示例

      jsx
      function App() {
        return (
          <div>
            <p>Hello</p>
            <p>World</p>
          </div>
        );
      }
      
      • 如果这两个 <p> 节点都需要插入 DOM,React 会一次性将它们插入父节点,而不是分两次操作。
  3. 不可中断的提交

    • Commit 阶段是 不可中断的,必须一次性完成所有 DOM 操作,以确保更新的原子性(Atomicity)。
    • 这与 Reconciliation 阶段的可中断性形成对比。