首页 / 文章

虚拟 DOM 差异对比揭秘:React 比较了什么以及为何这么做有意义

了解 React 的虚拟 DOM 真正是什么,对比机制如何处理两个元素树之间的差异,提交阶段会发生哪些变化,以及性能优势源自何处。

发布者:REACTAPP.TOP阅读约 4 分钟1032 词

来源: How React Virtual DOM Works Under the Hood

界面会不断变化:出现通知、按钮状态切换、计数器递增、列表重新渲染。所有这些变化都必须传递到浏览器的 DOM 中,而粗心地更新 DOM 是导致页面运行缓慢的常见原因。React 的解决方案是虚拟 DOM 以及一种名为“协调”的差异对比机制。本文将解释 React 在内存中存储了什么、它是如何判断哪些内容发生了变化,以及为何这样做能带来好处,从而帮助你用准确的思维模型来理解更新过程。

为何直接操作 DOM 会降低效率

在 React 出现之前,UI 更新是命令式的:找到某个节点,然后修改它。

document.getElementById("title").innerText = "Hello";

在页面规模较小的情况下这没问题。但当有数百甚至数千个元素频繁变化时,不仅速度会变慢,管理起来也会十分困难,因为 DOM 的变化可能会迫使浏览器重新执行:

  • 样式和布局计算
  • 周围内容的重新排版
  • 重绘受影响的像素
  • 重新渲染页面的某些部分
  • 每次,浏览器都会重新计算元素的位置、大小以及需要重绘的像素。这项工作比创建或比较 JavaScript 对象要耗费更多资源。除此之外,许多独立操作 DOM 的代码还会导致屏幕显示与应用程序状态不同步。

    虚拟 DOM 究竟是什么

    真实的 DOM

    真实的 DOM 是浏览器根据标记生成的实时节点树,每个元素对应一个节点。即便是这样:

    <body>
      <h1>Hello</h1>
    </body>
    

    也会变成与布局和绘制相关的内部树结构,频繁修改它就会引发上述的高成本操作。

    虚拟表示

    虚拟 DOM 常被视作真实 DOM 的轻量级副本。更准确地说,它是对界面应呈现状态的描述:即组件在渲染时返回的普通对象。React 会先更新这一描述,之后再操作浏览器。一个元素大致如下所示:

    {
      type: "h1",
      props: {
        children: "Hello"
      }
    }
    

    这类对象的创建和比较成本很低,因此 React 可以在执行任何真实的 DOM 操作之前判断出哪些内容发生了变化。JSX 仅仅是用于生成这些对象的语法。

    从首次渲染到状态变化

    当像这样的组件首次被渲染时:

    function App() {
      return <h1>Hello World</h1>;
    }
    

    React 会构建虚拟树,创建对应的 DOM 节点并将其插入页面中。此时两棵树的状态一致,界面便得以显示。

    之后,某个点击处理函数可能会更新状态:

    setCount(count + 1);
    

    或者父组件传递了新的属性。这表明部分用户界面可能需要更改。React不会立即修改DOM,而是安排重新渲染,并且通常会将来自同一事件的所有更新批量处理。

    在重新渲染过程中,React会再次调用该组件并获取新的虚拟树结构,这样它就同时拥有旧版本和新版本的结构。旧的虚拟树结构如下:

    <h1>Hello</h1>
    

    新的虚拟树结构如下:

    <h1>Hello Omer</h1>
    

    任务就是准确判断出哪些部分发生了变化。

    协调机制:差异比较的原理

    协调机制指的是将之前的虚拟树结构与新的虚拟树结构进行比较;这种比较过程通常被称为差异检测。对于每个位置,React都会判断该元素是发生了变化、保持不变,还是被添加或删除了。从概念上来说:

    Old Tree  VS  New Tree
    

    目标是以最少的改动将旧用户界面转换为新界面。完全通用的树形差异对比效率过低,因此 React 采用了两种策略。如果某个元素的类型发生变化,例如从 div 变为 span,React 会替换整个子树而非仅匹配其子元素。在列表中,React 通过 key 来匹配子元素,这也解释了为什么稳定的键值如此重要:它们能告知 React 是元素被移动而非替换。

    仅文本发生变化时

    之前:

    <h1>Hello</h1>
    

    之后:

    <h1>Hello Omer</h1>
    

    <h1> 在相同位置类型不变,仅有文本内容不同。因此 React 会保留现有的 DOM 节点,仅更新文本节点而非重新创建该元素。避免执行所有非必要的操作正是优化的核心所在。

    提交阶段:仅应用已更改的内容

    在完成差异对比后,React会将生成的变更写入真实的DOM中:

    • 更新文本
    • 设置或移除属性
    • 插入新元素
    • 删除已消失的元素

    这就是提交阶段,也是React唯一会操作DOM的环节,它只会修改差异对比中标记出的内容。随后浏览器会重新绘制受影响的区域。如需更深入地了解从提交到像素的转换过程,请参阅我们关于React从状态更新到屏幕像素渲染的指南。

    性能优势的来源

    其优势在于减少了昂贵的 DOM 操作。React 并不会重新构建所有内容,而是比较旧版本与新版本的描述,计算出最小的差异,仅修改受影响的节点。由于对象比较的成本远低于 DOM 变更,因此在大型应用中这种做法通常能带来显著效益。

    有两个注意事项有助于保持这一模型的真实性。虚拟 DOM 本身并不一定比精心调优的 DOM 代码更快;它的价值在于让开发者能够以声明式方式编写 UI,同时仍能实现高效更新。此外,渲染并非无需成本:React 仍然会调用组件并比较它们的输出,因此避免不必要的重新渲染依然非常重要。

    核心要点

    • 虚拟 DOM 是一组用于描述预期 UI 的普通对象,会在每次渲染时被重新创建。
  • 通过元素类型和key作为快捷方式,对比旧版本与新版本的差异。
  • React仅在提交阶段写入DOM,且仅记录其中的差异部分。
  • 你虽然需要承担一些渲染和差异对比的工作,但能大幅减少代价高昂的DOM操作,并获得更简单的编程模型。