人人都会AI编程

5.1 虚拟 DOM 的本质:用 JavaScript 对象描述 DOM 结构

更新时间:2026-07-10

在 React 15 及更早版本中,React 使用 Stack Reconciler(栈协调器) 来处理虚拟 DOM 的对比和更新。它的核心特征是同步、不可中断的递归渲染——一旦开始更新,就必须一次性完成整个组件树的对比和渲染,中间不能暂停,也无法让出主线程。

同步渲染的工作方式

当组件状态发生变化时,Stack Reconciler 会从根组件开始,递归地遍历整个虚拟 DOM 树,逐层对比新旧节点,计算差异,并在同一次调用栈中完成所有真实 DOM 的更新。整个过程是一气呵成的,直到整个组件树更新完毕,浏览器才有机会去处理其他任务(如响应用户输入、执行动画帧、绘制新页面)。

用户点击 → React 开始更新 → 递归遍历整棵组件树 → 完成所有 DOM 操作 → 浏览器重新绘制
   ↑                                                                                ↓
   └────────────────── 这段时间内浏览器完全被阻塞,无法响应用户 ───────────────────┘

卡顿问题的根源

这种同步渲染在大型应用中会直接导致页面卡顿甚至无响应。核心原因在于 JavaScript 是单线程的,React 的更新过程如果长时间占用主线程,浏览器就无法执行帧渲染、用户事件处理等关键任务。

举个例子:假设有一个包含大量列表项和复杂嵌套组件的页面,当用户快速输入关键词进行搜索过滤时,每次按键都会触发整个页面的重新渲染计算。如果一次渲染耗时 200ms,那么用户就会明显感觉到输入卡顿,界面跟不上打字速度。

// 一个包含大量子组件的复杂页面
function Dashboard() {
  const [keyword, setKeyword] = useState('');

  // 每次输入都会触发整个 Dashboard 及其所有子组件重新渲染
  return (
    <div>
      <input value={keyword} onChange={e => setKeyword(e.target.value)} />
      <HeavyDataTable filter={keyword} />
      <ComplexChart filter={keyword} />
      <ActivityLog filter={keyword} />
    </div>
  );
}

在 Stack Reconciler 下,每一次 setKeyword 都会同步地重新计算 HeavyDataTableComplexChartActivityLog 这三棵组件树的虚拟 DOM 差异。如果这些组件内部状态复杂、子组件数量多,单次更新就可能远超一帧的时间预算(16ms),导致掉帧和卡顿。

更深层的问题:无法区分更新优先级

Stack Reconciler 的另一个致命缺陷是无法区分高优先级更新和低优先级更新。所有状态变更都被一视同仁地立即处理,无论它们来自:

  • 高优先级:用户输入、按钮点击、动画交互
  • 低优先级:服务器返回的数据、后台计算结果的展示

这意味着一个本该立刻响应的用户点击,可能被一个正在进行的大列表渲染阻塞,直到整个列表渲染完成才有反应。这种体验在复杂应用中会变得完全不可接受。

实际开发中的典型症状

使用 React 15 开发中大型应用时,你大概率会遭遇以下问题:

  1. 输入框延迟:在包含大量数据的页面中输入,字符出现肉眼可见的滞后。
  2. 动画卡顿:CSS 动画或 requestAnimationFrame 驱动的动效在状态更新期间会不流畅。
  3. 长时间白屏或无响应:路由切换或数据加载时,界面完全“冻住”,无法进行任何操作。

这些问题的共同根源就是 Stack Reconciler 的同步递归更新机制——它强行占用了主线程,剥夺了浏览器响应高优先级任务的能力。

为什么需要新的架构

Stack Reconciler 的痛点暴露了 React 在复杂场景下的性能上限:“全量同步更新”无法适应现代交互对响应速度的要求。为了解决这个问题,React 团队重写了核心协调算法,引入了 Fiber 架构——一种能够将渲染工作拆分为多个小任务,并支持暂停、恢复和优先级调度的可中断渲染引擎。这将是下一节详细展开的内容。

理解 Stack Reconciler 的局限性,是理解 Fiber 设计动机的关键。正是这些实际场景中的卡顿和无响应,推动了 React 从“同步不可中断”向“异步可中断”的架构演进。