2025年03月12日10 分钟

React 渲染过程深度解析:从 JSX 到真实 DOM

深入 React 的渲染机制,详解 Fiber 架构、Reconciliation 算法、双缓冲和 Commit 阶段

React 渲染的全景图

    JSX 代码
       │
       ▼  Babel/SWC 编译
    React.createElement() 调用
       │
       ▼
    React Element(虚拟 DOM 对象)
       │
       ▼  React 内部处理
  ┌────────────┐
  │ Render 阶段 │  构建 Fiber 树 + Diff 算法
  │ (可中断)    │  确定哪些 DOM 需要变更
  └─────┬──────┘
        │  生成 Effect List
        ▼
  ┌────────────┐
  │ Commit 阶段 │  执行 DOM 操作
  │ (不可中断)   │  执行生命周期/Hooks
  └─────┬──────┘
        │
        ▼
    真实 DOM 更新

第一步:JSX → React Element

// 你写的 JSX
function App() {
  return (
    <div className="app">
      <h1>Hello</h1>
      <Counter count={0} />
    </div>
  )
}

// Babel 编译后
function App() {
  return React.createElement(
    'div',
    { className: 'app' },
    React.createElement('h1', null, 'Hello'),
    React.createElement(Counter, { count: 0 })
  )
}

// React.createElement 返回的对象(React Element)
{
  $$typeof: Symbol(react.element),
  type: 'div',
  props: {
    className: 'app',
    children: [
      { $$typeof: Symbol(react.element), type: 'h1', props: { children: 'Hello' } },
      { $$typeof: Symbol(react.element), type: Counter, props: { count: 0 } },
    ]
  },
  key: null,
  ref: null,
}

React Element 只是一个普通的 JS 对象,描述了"我想要什么样的 UI"。它不是 DOM 节点,也没有任何实例方法。

第二步:React Element → Fiber

什么是 Fiber?

Fiber 是 React 16 引入的新架构。每个 React Element 对应一个 Fiber 节点,Fiber 节点形成一棵树。

React Element 树(描述结构):        Fiber 树(工作单元):
     div                              FiberNode(div)
    /   \                              │ child
   h1   Counter                        ▼
                                   FiberNode(h1)
                                       │ sibling
                                       ▼
                                   FiberNode(Counter)

Fiber 节点的结构

interface FiberNode {
  // === 静态结构 ===
  tag: number           // 组件类型(函数组件=0, 类组件=1, 原生DOM=5...)
  type: any             // 'div' | 'h1' | Counter(函数引用)
  key: string | null

  // === 树结构(链表)===
  return: FiberNode     // 父节点
  child: FiberNode      // 第一个子节点
  sibling: FiberNode    // 下一个兄弟节点

  // === 工作单元 ===
  pendingProps: object  // 新的 props
  memoizedProps: object // 上次渲染的 props
  memoizedState: any    // 上次渲染的 state(Hooks 链表)

  // === 副作用 ===
  flags: number         // 标记:需要插入(2)、更新(4)、删除(8)...
  subtreeFlags: number  // 子树的副作用标记

  // === 双缓冲 ===
  alternate: FiberNode  // 指向另一棵树上的对应节点
}

为什么用链表而不是树?

普通树结构(递归遍历,不可中断):
  function traverse(node) {
    process(node)
    node.children.forEach(traverse)  // 递归调用栈越来越深
    // 无法在中间暂停!
  }

Fiber 链表(循环遍历,可中断):
  function workLoop() {
    while (currentFiber && !shouldYield()) {
      currentFiber = performUnitOfWork(currentFiber)
    }
    // shouldYield() 检查是否该让出主线程
    // 如果要中断,记住 currentFiber,下次继续
  }

第三步:Render 阶段(Reconciliation)

双缓冲机制

React 维护两棵 Fiber 树:

 current 树(当前显示的)         workInProgress 树(正在构建的)
      div                              div
     /   \                            /   \
    h1   Counter                     h1   Counter
    │      │                         │      │
  "Hello"  count=0                 "Hello"  count=1  ← 更新

                commit 之后
                    ↓
 current 树(更新后的)            旧树变成下次的 workInProgress
      div                              div
     /   \                            /   \
    h1   Counter                     h1   Counter
    │      │                         │      │
  "Hello"  count=1                 "Hello"  count=0

beginWork:自上而下构建

// 对每个 Fiber 节点执行
function beginWork(current, workInProgress) {
  switch (workInProgress.tag) {
    case FunctionComponent:
      // 执行函数组件 → 调用你的组件函数
      // 执行 Hooks(useState, useEffect 等)
      const children = Component(workInProgress.pendingProps)
      reconcileChildren(current, workInProgress, children)
      break

    case HostComponent: // 原生 DOM 元素
      // 处理 props,创建子 Fiber
      reconcileChildren(current, workInProgress, workInProgress.pendingProps.children)
      break
  }

  return workInProgress.child  // 返回下一个要处理的节点
}

Diff 算法(reconcileChildren)

React 的 Diff 有三个关键策略来将 O(n³) 降为 O(n):

策略1:只比较同层级节点
  旧:  div → [A, B, C]
  新:  div → [A, C, D]
  只在同一层比较,不会跨层移动

策略2:不同类型的元素 → 直接替换整棵子树
  旧:  <div><Counter/></div>
  新:  <span><Counter/></span>
  div 变成 span → 销毁整个旧子树,创建新子树

策略3:用 key 标识列表中的元素
  旧:  [<li key="a"/>, <li key="b"/>, <li key="c"/>]
  新:  [<li key="c"/>, <li key="a"/>, <li key="b"/>]
  通过 key 识别:c 移到前面,a 和 b 不变
  而不是认为三个元素都变了

列表 Diff 的详细过程

旧列表: A → B → C → D
新列表: A → C → D → B

第1轮:从头比较
  A === A ✅ 可复用

第2轮:头不匹配(B !== C),进入 Map 查找
  建立 Map: { B: FiberB, C: FiberC, D: FiberD }

  遍历新列表剩余:
  C → Map 中找到 FiberC → 标记"移动"
  D → Map 中找到 FiberD → 标记"移动"
  B → Map 中找到 FiberB → 标记"移动"

  Map 中剩余:无 → 无需删除

结果 Effect List:
  FiberA: 无变化
  FiberC: 移动
  FiberD: 移动
  FiberB: 移动

completeWork:自下而上收集

function completeWork(workInProgress) {
  switch (workInProgress.tag) {
    case HostComponent:
      if (current !== null) {
        // 更新:对比新旧 props,记录需要更新的属性
        const updatePayload = diffProperties(oldProps, newProps)
        // 如 [['className', 'new-class'], ['style', { color: 'red' }]]
        if (updatePayload) {
          workInProgress.flags |= Update  // 标记需要更新
        }
      } else {
        // 新建:创建 DOM 节点(但不插入文档)
        const instance = document.createElement(workInProgress.type)
        workInProgress.stateNode = instance
        workInProgress.flags |= Placement  // 标记需要插入
      }
      break
  }

  // 向上冒泡副作用标记
  workInProgress.return.subtreeFlags |= workInProgress.subtreeFlags
  workInProgress.return.subtreeFlags |= workInProgress.flags
}

第四步:Commit 阶段

Commit 阶段不可中断,一次性执行所有 DOM 操作。

Commit 阶段的三个子阶段:

  ┌──────────────────┐
  │ Before Mutation   │  读取 DOM 快照
  │                   │  getSnapshotBeforeUpdate
  └────────┬─────────┘
           │
  ┌────────▼─────────┐
  │ Mutation          │  执行 DOM 操作
  │                   │  插入、更新、删除
  └────────┬─────────┘
           │
  ┌────────▼─────────┐
  │ Layout            │  DOM 已更新
  │                   │  useLayoutEffect
  │                   │  componentDidMount/Update
  └────────┬─────────┘
           │
           ▼
    浏览器绘制
           │
  ┌────────▼─────────┐
  │ Passive Effects   │  异步执行
  │                   │  useEffect
  └──────────────────┘

Mutation 阶段的 DOM 操作

function commitMutation(fiber) {
  if (fiber.flags & Placement) {
    // 插入新节点
    const parent = fiber.return.stateNode
    parent.appendChild(fiber.stateNode)
  }

  if (fiber.flags & Update) {
    // 更新属性
    const { updatePayload } = fiber
    for (let i = 0; i < updatePayload.length; i += 2) {
      const propKey = updatePayload[i]
      const propValue = updatePayload[i + 1]
      if (propKey === 'style') {
        Object.assign(fiber.stateNode.style, propValue)
      } else if (propKey === 'className') {
        fiber.stateNode.className = propValue
      } else {
        fiber.stateNode.setAttribute(propKey, propValue)
      }
    }
  }

  if (fiber.flags & Deletion) {
    // 删除节点
    const parent = fiber.return.stateNode
    parent.removeChild(fiber.stateNode)
  }
}

并发模式下的调度

React 18 的并发模式让渲染可以被中断和恢复

高优先级更新(用户输入)vs 低优先级更新(数据加载)

场景:用户在搜索框输入,同时有大量列表需要更新

同步模式(React 17):
  [======== 列表渲染 ========] [用户输入响应]
                                     ↑ 卡顿 500ms

并发模式(React 18):
  [列表渲染...] 中断 [用户输入响应] [继续列表渲染...]
                      ↑ 即时响应
// React 内部的调度循环
function workLoopConcurrent() {
  while (workInProgress !== null && !shouldYield()) {
    workInProgress = performUnitOfWork(workInProgress)
  }
  // shouldYield():检查是否有更高优先级的任务
  // 如果有 → 暂停当前渲染 → 处理高优先级任务 → 回来继续
}

一次完整更新的全流程示例

function Counter() {
  const [count, setCount] = useState(0)
  return <button onClick={() => setCount(count + 1)}>{count}</button>
}

用户点击按钮:

1. onClick 触发 → setCount(1)
2. React 创建更新对象 { state: 1 }
3. 调度器安排更新(concurrent: 根据优先级调度)

=== Render 阶段 ===

4. beginWork(Counter Fiber)
   → 执行 Counter()
   → useState 处理更新队列 → state = 1
   → 返回 React Element: { type: 'button', props: { children: 1 } }

5. reconcileChildren
   → 旧: <button>0</button>
   → 新: <button>1</button>
   → Diff: type 相同(button),children 变化(0→1)
   → 标记 button Fiber: flags |= Update

6. completeWork
   → diffProperties: [['children', 1]]
   → 向上冒泡 subtreeFlags

=== Commit 阶段 ===

7. Before Mutation: (无操作)

8. Mutation:
   → button.textContent = 1

9. Layout:
   → 切换 current 指针到新 Fiber 树
   → 执行 useLayoutEffect(如果有)

10. 浏览器绘制 → 用户看到 1

11. Passive Effects:
    → 执行 useEffect(如果有)

总结

React 渲染的核心设计:

阶段工作内容可中断?
JSX 编译JSX → createElement 调用构建时
Element 创建生成虚拟 DOM 对象同步
Render(beginWork)执行组件、Diff 算法、构建 Fiber✅ 可中断
Render(completeWork)收集副作用、创建 DOM 节点✅ 可中断
Commit(Mutation)执行 DOM 操作❌ 不可中断
Commit(Layout)useLayoutEffect、生命周期❌ 不可中断
Passive EffectsuseEffect异步