Published on

React 内部渲染机制

React 以其声明式的 UI 范式,极大地简化了复杂界面的开发。开发者只需描述在特定状态下 UI 应该呈现的样子,而 React 则负责高效地将这些声明转化为实际的 DOM 更新。这一过程的背后,是一套精密、高效且不断演进的内部工作管线。自 React 16 引入 Fiber 架构以来,其渲染机制变得更加强大,能够支持并发渲染 (Concurrent Rendering) 等高级特性。

渲染管线的四个核心阶段

React 渲染管线

一次 React 更新的完整生命周期,可以被抽象为四个连续的阶段。

阶段一:触发 (Trigger)

渲染的起点是状态的变更。当一个组件的 stateprops 发生变化时(例如,通过 setState 调用),一个更新就被触发了。

  • 机制: React 内部会调用 scheduleUpdateOnFiber() 来标记需要更新的组件(在其对应的 Fiber 节点上)。这个过程会确定更新的优先级(例如,由用户交互触发的更新优先级更高),并最终通过 ensureRootIsScheduled() 来创建一个更新任务。
  • 输出: 此阶段的最终产物是一个待处理的更新任务,它将被传递给下一阶段的调度器。
更新 (Update) 的来源

触发一次 React 渲染的来源是多样化的,主要包括:

  • useStatesetter 函数调用。
  • useReducerdispatch 函数调用。
  • ReactDOM.createRoot(rootNode).render(<App />) 的初次渲染或后续调用。

阶段二:调度 (Schedule)

  • 机制: 调度器本质上是一个优先级队列 (priority queue)。它接收来自 React 核心的任务,并根据任务的优先级(如交互式更新、过渡更新等)来决定它们的执行顺序。
    • scheduleCallback(): 这是调度器的入口函数,用于将任务(如渲染或 effect 执行)添加到队列中。
    • workLoop(): 这是调度器内部的任务循环,它会根据优先级从队列中取出最高优先级的任务并执行它,直到队列为空或时间片用尽。
  • 目的: 调度器的核心目标是确保高优先级的更新(如用户输入响应)能够优先得到处理,而低优先级的更新(如后台数据获取)则可以在浏览器空闲时执行,从而保证应用的响应性。

阶段三:渲染 (Render)

这是 React 最核心、最复杂的阶段,也称为协调 (Reconciliation)。在此阶段,React 会“计算”出两次渲染之间 UI 究竟发生了哪些变化。

  • 机制: 调度器会调用 performConcurrentWorkOnRoot 等函数来启动渲染阶段。React 会从应用的根 Fiber 节点开始,遍历整个组件树,为所有被标记为需要更新的组件,调用其渲染函数(或 render 方法),从而构建出一棵新的、代表更新后 UI 状态的 “工作进度 (work-in-progress)” Fiber 树
  • Diffing 算法: 在构建新树的过程中,React 会将其与当前已渲染的 Fiber 树进行对比 (diffing),找出两者之间的差异,并生成一个包含了所有需要执行的 DOM 操作(如增、删、改)的副作用列表 (effect list)
Fiber 树 (Fiber Tree)

Fiber 是 React 16+ 中协调算法的核心数据结构。它将原先不可中断的递归式 Virtual DOM diffing,重构为了一个可中断、可恢复的、基于链表的遍历过程。

每个 Fiber 节点代表一个工作单元,它包含了组件的类型、props、state、以及指向其父节点、子节点和兄弟节点的指针。这种结构使得 React 可以在渲染过程中暂停工作,处理更高优先级的任务,稍后再回来继续。

  • 并发特性: 渲染阶段在并发模式下是异步的、可中断的。React 可以根据任务的优先级和可用的时间片,将渲染工作分解成多个小块执行,甚至在完成前被更高优先级的任务打断。

阶段四:提交 (Commit)

当渲染阶段成功完成,并生成了完整的副作用列表后,React 会进入同步的、不可中断·的提交阶段。提交阶段会将渲染阶段计算出的所有变更,一次性地应用到宿主环境(在 Web 中即浏览器 DOM)。这个过程主要由 commitRoot 函数协调:

Effect 的执行时机

useLayoutEffectuseEffect 的核心区别正在于它们在提交阶段的执行时机。

import { useState, useLayoutEffect, useEffect } from 'react';

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

  // Layout Effect 在 DOM 更新后、浏览器绘制前同步执行
  useLayoutEffect(() => {
    console.log('LayoutEffect: DOM has been updated, but browser has not painted yet.');
    // 适合执行需要同步读取 DOM 布局并可能再次触发同步渲染的操作
  }, [count]);

  // Passive Effect 在浏览器绘制完成后异步执行
  useEffect(() => {
    console.log('Effect: Browser has painted the changes.');
    // 适合绝大多数副作用,不会阻塞浏览器绘制
  }, [count]);

 return <div onClick={() => setCount(c => c + 1)}>Count: {count}</div>;
}