深入了解 React 中的虚拟 DOM
虚拟 DOM 是 React 的一个基本概念。如果你在过去几年写过 React 代码,你可能听说过它。然而,你可能不理解它是如何工作的以及 React 为什么使用它。
本文将介绍什么是虚拟 DOM,它在 React 中的好处,以及帮助解释这个概念的实际示例代码。
1. 概念回顾:什么是 DOM
为了理解虚拟 DOM 并了解 React 实现它的原因,让我们回顾一下实际浏览器 DOM 的知识。
每当浏览器加载一个 web 文档(如 HTML)时,文档元素基于对象的表示就会以树状结构创建。这种对象表示称为文档对象模型,也称为 DOM。由于其基于对象的特性,JavaScript 和其他脚本语言理解 DOM,并可以交互和操作文档内容。例如,使用 DOM,开发人员可以添加或删除元素,修改它们的外观,并在 web 元素上执行用户操作。DOM 查询和更新等 DOM 操作更轻,因此非常快。然而,为了使更新反映在网页上,页面将不得不重新渲染。
2. 重新渲染如何影响性能
重新渲染页面以反映 DOM 更新的成本很高,而且可能导致性能不足,因为浏览器必须重新计算 CSS,为每个可见元素重新运行布局,并重新绘制网页。
让我们用下面的 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 树如下所示:
通过在代码中使用 setInterval()
回调函数,我们可以每秒渲染 UI 的状态。正如我们在下面的 GIF 中看到的,在指定的间隔后,浏览器重新渲染,运行布局,重新绘制网页,以及其他操作。
浏览器 DOM 没有机制来比较和对比已经更改的内容,只重绘 DOM 节点(在本例中是渲染时间):
这种重新渲染在文本输入中很明显。正如我们所看到的,输入字段总是在设置的间隔之后被清除。DOM 操作之后浏览器中的重新渲染过程会导致性能不足。
3. React 中的重渲染:为什么使用虚拟 DOM
正如我们所知,React 是一个基于组件的库。如果 state
或 prop
发生变化,或者其父组件重新渲染,React 组件将自然地重新渲染。
React 无法承担每次重新渲染后重新绘制所有 DOM 节点的成本。为了克服这个挑战,React 实现了虚拟 DOM 的概念。
React 不允许浏览器在每次重新渲染或 DOM 更新后重新绘制所有页面元素,而是使用虚拟 DOM 的概念,在不涉及实际 DOM 的情况下找出究竟发生了什么变化,然后确保实际 DOM 只重新绘制必要的数据。这个概念帮助 React 优化性能。
4. React 中的虚拟 DOM
React 中的虚拟 DOM 是实际 DOM 的“虚拟”表示。它只是一个用于复制实际 DOM 的对象。与实际的 DOM 不同,虚拟 DOM 的创建成本很低,因为它不写入屏幕。它只能作为一种策略,以防止在重新渲染时重绘不必要的页面元素。
看看下面的渲染代码,它代表了前面 JavaScript 例子的 React 版本:
// ... const update = () => { const element = ( <> <h3>React:</h3> <form> <input type="text" /> </form> <span>Time: {new Date().toLocaleTimeString()}</span> </> ); root.render(element); };
为了简洁起见,我们删除了一些代码。我们也可以在普通 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 编辑器中,可以获得与 JSX 代码等价的 React 代码。
现在,如果我们在控制台中记录 React 元素:
const element = ( <> <h3>React:</h3> <form> <input type="text" /> </form> <span>Time: {new Date().toLocaleTimeString()}</span> </> ); console.log(element)
我们会得到这样的东西:
如上所示,该对象是虚拟 DOM。
5. React 如何实现虚拟 DOM
当我们渲染用户界面时,为该渲染创建一个虚拟 DOM 并保存在内存中。如果在中渲染发生更新,React 会自动为更新创建一个新的虚拟 DOM 树。
为了帮助进一步解释这一点,让我们像这样直观地表示虚拟 DOM:
但是,不要忘记虚拟 DOM 只是一个表示 UI 的简单对象。没有东西会被画在屏幕上,所以,它很容易创建。
在 React 创建新的虚拟 DOM 树之后,它将使用 diff 算法将其与前一个虚拟 DOM 树进行比较,以确定需要进行哪些更改。然后,它再确保实际的 DOM 只接收和重绘更新的节点。这个过程叫做 reconciliation。
- 当 React 实现 diff 算法时,它首先比较两个快照是否具有相同的根元素。如果它们具有相同的元素,则 React 继续向前并递归处理属性,然后是 DOM 节点的子节点。
- 如果根元素是不同类型的,这在大多数更新中是罕见的,React 将销毁旧的 DOM 节点并构建一个新的 DOM 树。
如果我们检查我们的 React 渲染,我们将得到以下行为:
在每次渲染时,React 都有一个虚拟 DOM 树,它会与以前的版本进行比较,以确定更新了哪些节点内容,并确保更新的节点与实际的 DOM 匹配。
在上面的 GIF 中,我们可以看到只有状态改变的渲染时间在每次重渲染时被重新绘制。
在下面的另一个例子中,我们渲染了一个简单的 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 只知道更新类名和更改的文本。
6. 虚拟 DOM 在 React 中使用的原因
每当我们在 React 中操作虚拟 DOM 元素时,我们都绕过了直接操作实际 DOM 时所涉及的一系列操作。这是可能的,因为使用虚拟 DOM,不会在屏幕上绘制任何东西。此外,通过 diff 算法,React 可以确定需要更新什么,只更新真正 DOM 上的对象。
React 中的虚拟 DOM 概念无疑有助于降低重新渲染网页的性能成本,从而将重新绘制屏幕所需的时间最小化。
这里有一个简单的类比,可以进一步巩固我们对虚拟 DOM 的知识:将操纵虚拟 DOM 看作是编辑结构设计或蓝图,而不是重新构建实际的结构。与每次发生更新时重新构建结构相比,编辑蓝图以包含更新非常便宜。当蓝图被修改和最终确定后,我们就可以只包含对实际结构的更新。
7. 小结
虚拟 DOM 只是 React 用来优化应用程序性能的一种策略。它提供了一种比较两个渲染树的机制,以了解究竟发生了什么变化,并且只更新实际 DOM 中必要的内容。
与 React 一样,Vue 和其他一些框架也采用了这种策略。然而,Svelte 框架提出了另一种方法来确保应用程序得到优化。相反,它将所有组件编译成独立的、微小的 JavaScript 模块,使脚本运行起来非常轻便和快速。