优秀的编程知识分享平台

网站首页 > 技术文章 正文

什么是 React 中的虚拟 DOM(react virtual dom)

nanyue 2024-10-01 13:12:38 技术文章 8 ℃

什么是 React 中的虚拟 DOM


虚拟 DOM 是一个基本的 React 概念;如果你在过去几年里写过任何 React 代码,你可能听说过它。但是,您可能不了解它是如何工作的以及 React 为什么使用它。

在本文中,我们将介绍虚拟 DOM 是什么,探索它在 React 中的好处并回顾一个实际示例。让我们开始吧!

跳跃前进:

  • 什么是 React DOM?
  • 重新渲染如何影响性能
  • 探索 React 虚拟 DOMReact 虚拟 DOM 与真正的 DOM 有何不同?虚拟 DOM 对象React 如何实现虚拟 DOM反应差异过程React 如何差异列表
  • React virtual DOM vs. shadow DOM
  • React virtual DOM vs. real DOM
  • 比较图表:真实 DOM vs. 虚拟 DOM vs. 影子 DOM

什么是 React DOM?

为了理解虚拟 DOM 并了解 React 实现它的原因,让我们首先刷新我们对实际浏览器 DOM 是什么的知识。

通常,每当用户请求网页时,浏览器都会从服务器接收该页面的 HTML 文档。然后,浏览器从 HTML 构造一个逻辑的树状结构,以在客户端中向用户显示请求的页面。

这种树状结构称为文档对象模型,也称为 DOM。它是 Web 文档作为节点和对象的结构表示形式,在本例中为 HTML 文档。

DOM 充当 Web 文档的接口,以便 JavaScript 和其他脚本语言可以访问、操作文档内容并以编程方式与文档内容进行交互。例如,开发人员可以使用 DOM API 添加或删除元素、修改其外观以及对 Web 元素执行用户操作。

重新渲染如何影响性能

DOM 操作非常快速、轻便。但是,当应用数据更改并触发更新时,重新呈现可能会很昂贵。

让我们模拟使用以下 JavaScript 代码重新渲染页面:

const update = () => {
 const element = `
  <h3>JavaScript:</h3>
  <form>
   <input type="text"/>
  </form>
  <span>Time: ${new Date().toLocaleTimeString()}</span>
 `;

 document.getElementById("root1").innerHTML = element;
};

setInterval(update, 1000);

您可以在 上找到完整的代码。表示文档的 DOM 树如下所示:

代码中的回调允许我们在每秒触发 UI 的模拟重新渲染。如下面的 GIF 所示,文档 DOM 元素在每次更新时都会重新构建和重新绘制。由于这种重新呈现,UI 中的文本输入也会失去其状态:setInterval()

如上所示,当 UI 中发生更新时,文本字段会丢失输入值,这需要优化。

不同的 JavaScript 框架提供了不同的解决方案和策略来优化重新渲染。但是,React 实现了虚拟 DOM 的概念。

探索 React 虚拟 DOM

顾名思义,虚拟 DOM 是以对象的形式对实际 DOM 的轻量级复制品。虚拟 DOM 可以保存在浏览器内存中,不会直接更改用户浏览器上显示的内容。由其他几个前端框架(如 Vue)实现,React 的声明式方法是独一无二的。


虚拟 DOM 与真实 DOM 有何不同?

一个常见的误解是虚拟 DOM 比实际 DOM 快或与之相媲美,然而,这是不正确的。事实上,虚拟 DOM 的操作支持并添加到实际 DOM 的操作中。 本质上,虚拟 DOM 提供了一种机制,允许实际 DOM 在重新呈现 UI 时计算最少的 DOM 操作。

例如,当实际 DOM 中的元素发生更改时,DOM 将重新呈现该元素及其所有子元素。在构建具有大量交互性和状态更改的复杂 Web 应用程序时,这种方法速度缓慢且效率低下。

相反,在渲染过程中,React 采用了虚拟 DOM 的概念,这符合其声明式方法。因此,我们可以指定我们希望 UI 处于什么状态,之后 React 会让它发生。

更新虚拟 DOM 后,React 将其与更新前拍摄的虚拟 DOM 快照进行比较,确定更改了哪个元素,然后只更新真实 DOM 上的该元素。这是虚拟 DOM 用来优化性能的一种方法。我们稍后会详细介绍。

虚拟 DOM 将手动 DOM 操作从开发人员那里抽象出来,帮助我们编写更可预测和更整洁的代码,以便我们可以专注于创建组件。

借助虚拟 DOM,您不必担心状态转换。一旦你更新了状态,React 就会确保 DOM 与该状态匹配。例如,在我们的上一个例子中,React 确保在每次重新渲染时,只在实际的 DOM 中更新。因此,在 UI 更新发生时,我们不会丢失输入字段的值。Time

虚拟 DOM 对象

让我们考虑以下渲染代码,它代表了我们之前 JavaScript 示例的 React 版本:

// ...
const update = () => {
 const element = (
  <>
   <h3>React:</h3>
   <form>
    <input type="text" />
   </form>
   <span>Time: {new Date().toLocaleTimeString()}</span>
  </>
 );
 root.render(element);
};

为简洁起见,我们删除了一些代码。您可以在 CodeSandbox 上查看完整的代码。我们还可以用普通的 React 编写 JSX 代码,如下所示:

const element = React.createElement(
 React.Fragment,
 null,
 React.createElement("h3", null, "React:"),
 React.createElement(
  "form",
  null,
  React.createElement("input", {
   type: "text"
  })
 ),
 React.createElement("span", null, "Time: ", new Date().toLocaleTimeString())
);

请记住,您可以通过将 JSX 元素粘贴到 Babel REPL 编辑器中来获取 React 等效的 JSX 代码。

现在,如果我们在控制台中记录 React 元素,我们最终会得到如下图所示的内容:

 const element = (
  <>
   <h3>React:</h3>
   <form>
    <input type="text" />
   </form>
   <span>Time: {new Date().toLocaleTimeString()}</span>
  </>
 );
 console.log(element)

如上所示,对象是虚拟 DOM。它表示用户界面。

React 如何实现虚拟 DOM

要理解虚拟 DOM 策略,我们需要了解所涉及的两个主要阶段,渲染和协调。

当我们渲染应用程序用户界面时,React 会创建一个代表该 UI 的虚拟 DOM 树并将其存储在内存中。在下一次更新时,或者换句话说,当渲染应用程序的数据发生变化时,React 将自动为更新创建一个新的虚拟 DOM 树。

为了进一步解释这一点,我们可以直观地表示虚拟 DOM,如下所示:

左侧的图像是初始渲染。随着变化,React 使用更新的节点创建了一个新树,如右侧所示。Time

请记住,虚拟 DOM 只是一个表示 UI 的对象,因此屏幕上不会绘制任何内容。

在 React 创建新的虚拟 DOM 树后,它会使用一种称为协调的差异算法将其与以前的快照进行比较,以确定需要进行哪些更改。

在协调过程之后,React 使用像 ReactDOM 这样的渲染器库,它获取不同的信息来更新渲染的应用程序。此库确保实际的 DOM 仅接收并重新绘制更新的一个或多个节点:

如上图所示,只有数据更改的节点才会在实际的 DOM 中重新绘制。下面的GIF进一步证明了这一说法:

当 UI 中发生状态更改时,我们不会丢失输入值。

总之,在每次渲染时,React 都会将虚拟 DOM 树与以前的版本进行比较,以确定哪个节点得到更新,确保更新的节点与实际的 DOM 匹配。

反应差异过程

当 React 比较两个虚拟 DOM 树时,它首先比较两个快照是否具有相同的根元素。如果它们具有相同的元素,就像在我们的例子中,更新的节点具有相同的元素类型,React 将继续移动并递归属性。span

在这两个快照中,元素上不存在或更新任何属性。然后 React 对子项重复该过程。看到文本节点发生了变化,React 只会更新真实 DOM 中的实际节点。spanTime

另一方面,如果两个快照具有不同的元素类型,这在大多数更新中很少见,React 将销毁旧的 DOM 节点并构建一个新的节点。例如,从 到 ,如下面的相应代码片段所示:spandiv

<span>Time: 04:36:35</span> 
<div>Time: 04:36:38</div>

在下面的示例中,我们渲染一个简单的 React 组件,该组件在单击按钮后更新组件状态:

import { useState } from "react";

const App = () => {
 const [open, setOpen] = useState(false);

 return (
  <div className="App">
   <button onClick={() => setOpen((prev) => !prev)}>toggle</button>
   <div className={open ? "open" : "close"}>
    I'm {open ? "opened" : "closed"}
   </div>
  </div>
 );
};
export default App;

更新组件状态会重新呈现组件。但是,如下所示,在每次重新渲染时,React 只知道更新类名和更改的文本。此更新不会损害渲染中不受影响的元素:

查看代码沙盒上的代码和演示。

React 如何差异列表

当我们修改项目列表时,React 如何比较列表取决于项目是添加到列表的开头还是结尾。请考虑以下列表:

<ul> 
  <li>item 3</li>
  <li>item 4</li>
  <li>item 5</li>
</ul>

在下一次更新中,让我们在末尾附加一个,如下所示:item 6

<ul> 
  <li>item 3</li>
  <li>item 4</li>
  <li>item 5</li>
  <li>item 6</li>
</ul>

React 从顶部比较项目。它匹配第一项、第二项和第三项,并且只知道插入最后一项。这个计算对于 React 来说很简单。

但是,让我们在开头插入,如下所示:item 2

<ul> 
  <li>item 2</li>
  <li>item 3</li>
  <li>item 4</li>
  <li>item 5</li>
</ul>

同样,React 从顶部进行比较,并立即意识到这与更新的树不匹配。因此,它将该列表视为需要重建的全新列表。item 3item 2

我们希望 DOM 仅通过预置来计算最少的操作,而不是重建整个列表。React 让我们添加一个 prop 来唯一标识项目,如下所示:item 2key

<ul> 
  <li key="3">item 3</li>
  <li key="4">item 4</li>
  <li key="5">item 5</li>
</ul>

<ul> 
  <li key="2">item 2</li>
  <li key="3">item 3</li>
  <li key="4">item 4</li>
  <li key="5">item 5</li>
  <li key="6">item 6</li>
</ul>

通过上面的实现,React 会知道我们已经预置和附加了 .因此,保留已可用的项目并仅添加 DOM 中的新项目将起作用。item 2item 6

如果我们在映射渲染项目列表时省略道具,React 会在浏览器控制台中提醒我们。key

虚拟 DOM 与影子 DOM 有何不同?

在我们结束之前,这里有一个经常出现的问题。影子 DOM 和虚拟 DOM 一样吗?简短的回答是他们的行为是不同的。

影子 DOM 是实现 Web 组件的工具。以 HTML 元素为例:inputrange

<input type="range" />

这给了我们以下结果:

如果我们使用浏览器的开发人员工具检查元素,我们将只看到一个简单的元素。但是,在内部,浏览器封装并隐藏构成输入滑块的其他元素和样式。input

使用 Chrome DevTools,我们可以启用该选项以查看影子 DOM:Show user agent shadow DOMSettings

在上图中,元素内部元素的结构化树称为影子 DOM 树。它提供了一种将组件(包括样式)与实际 DOM 隔离的方法。#shadow-rootinput

因此,我们确信小部件或组件的样式(如上面的范围)无论在何处呈现都会保留。换句话说,它们的行为或外观永远不会受到真实DOM中其他元素样式的影响。input

比较图表:真实 DOM vs. 虚拟 DOM vs. 影子 DOM

下表总结了真实 DOM、虚拟 DOM 和影子 DOM 之间的差异:


真正的DOM

虚拟 DOM

暗影DOM

描述

网络文档的界面;允许脚本与文档交互

实际 DOM 的内存中副本

用于实现 Web 组件的工具,或用于确定范围的实际 DOM 中的独立 DOM 树

与开发人员的相关性

开发人员手动执行 DOM 操作来操作 DOM

开发人员不必担心状态转换;虚拟 DOM 将 DOM 操作从开发人员那里抽象出来。

开发人员可以创建可重用的 Web 组件,而不必担心托管文档中的样式冲突

谁使用它们

在浏览器中实现

由库和框架(如 React、Vue 等)使用。

由 Web 组件使用

项目复杂性

适用于没有复杂交互性的简单、中小型项目

适用于具有高度交互性的复杂项目

适用于交互性较低的简单到中型项目

CPU 和内存使用情况

与虚拟 DOM 更新相比,真正的 DOM 使用更少的 CPU 和内存

与真正的 DOM 更新相比,虚拟 DOM 使用更多的 CPU 和内存。

与虚拟 DOM 更新相比,影子 DOM 使用的 CPU 和内存更少

封装

不支持封装,因为组件可以在其范围之外进行修改

支持封装,因为组件不能在其范围之外修改

支持封装,因为组件不能在其范围之外修改

结论

React 使用虚拟 DOM 作为一种策略,在重新渲染 UI 时计算最少的 DOM 操作。它不与真正的DOM竞争或比真正的DOM更快。

虚拟 DOM 提供了一种机制,可以将手动 DOM 操作从开发人员那里抽象出来,帮助我们编写更可预测的代码。它通过比较两个渲染树来确定确切的更改,只更新实际 DOM 上所需的内容。

与 React 一样,Vue 也采用了这种策略。然而,Svelte提出了另一种方法来确保应用程序得到优化,将所有组件编译成独立的、微小的JavaScript模块,使脚本非常轻巧和快速运行。

希望您喜欢阅读本文。如果您有任何问题或贡献,请务必在评论部分分享您的想法。

最近发表
标签列表