拒绝八股文:关于虚拟 DOM 的几个关键问题虚拟 DOM 是现代前端框架如 React 和 Vue 的核心,本文将深入探
什么是虚拟 DOM ?
虚拟 DOM(Virtual DOM,VDOM)是一种编程概念。在这个概念里, UI 以一种理想化的,或者说“虚拟的”表现形式被保存于内存中,然后将真实的 DOM 与之保持同步。这个概念是由 React 率先开拓,随后被许多不同的框架采用,当然也包括 Vue。
例如:
<ul class="fruits">
<li>Apple</li>
<li>Orange</li>
<li>Banana</li>
</ul>
上述代码可以在 Virtual DOM 中表示如下:
// Virtual DOM representation
{
type: "ul",
props: {
"class": "fruits"
},
children: [
{
type: "li",
props: null,
children: [
"Apple"
]
},
{
type: "li",
props: null,
children: [
"Orange"
]
},
{
type: "li",
props: null,
children: [
"Banana"
]
}
]
}
虚拟 DOM 的工作原理?
虚拟 DOM 的工作原理主要涉及以下几个步骤:
- 数据变化监听:当应用的状态发生变化时,虚拟 DOM 框架会监听到这些变化。
- 新虚拟 DOM 树生成:状态更新后,框架会使用新的状态数据生成一个新的虚拟 DOM 树。
- 差异对比:框架通过算法(如Diff算法)比较新旧虚拟DOM树,找出差异。
- 补丁生成:根据差异对比的结果,生成一个补丁(更新指令),指明哪些部分需要更新。
- 真实DOM更新:框架将这些补丁应用到真实DOM上,只对变化的部分进行操作,从而更新页面内容。
虚拟 DOM 比真实 DOM 快多少?
不, 虚拟 DOM 并不比真实 DOM 快。虚拟 DOM 在底层也使用真实 DOM 来渲染页面或内容。因此虚拟 DOM 不可能比真实 DOM 快。它能够更快的唯一方式是,如果我们将其与一个效率更低的框架进行比较(在2013年前后确实有很多这样的框架!)
onEveryStateChange(() => {
document.body.innerHTML = renderMyApp();
});
那么为什么每个人都说虚拟 DOM 更快呢? 关于虚拟 DOM 性能的误解可以追溯到 React 的发布。在前 React 核心团队成员 Pete Hunt 于 2013 年发表的开创性演讲 Pete Hunt:React:重新思考典范实例的意义— JSConf EU
Pete Hunt 也很快进行了澄清:
React is not magic. Just like you can drop into assembler with C and beat the C compiler, you can drop into raw DOM operations and DOM API calls and beat React if you wanted to. However, using C or Java or JavaScript is an order of magnitude performance improvement because you don't have to worry...about the specifics of the platform. With React you can build applications without even thinking about performance and the default state is fast. React不是魔法。就像你可以通过使用C语言深入到汇编语言中,超越C编译器的性能一样,你也可以通过直接使用原始的DOM操作和DOM API调用来超越React,如果你愿意的话。然而,使用C或Java或JavaScript是一种数量级的性能提升,因为你不需要担心...平台的具体细节。使用React,你可以构建应用程序,甚至不需要考虑性能问题,而默认状态就是快速的。
实际上,除了主流的React和Vue框架外,还有许多不使用虚拟DOM的框架,它们也能提供非常优异的性能表现。以下是几个例子:
- Svelte:Svelte是一个编译型框架,它在构建时将组件代码转换成高效的原生JavaScript代码,从而避免了运行时的虚拟DOM开销。Svelte的特点是编译时优化,它不使用虚拟DOM,而是通过编译阶段的优化来实现高性能的Web应用。
- SolidJS:SolidJS是一个声明式的JavaScript库,用于构建高效的用户界面。它采用了类似于React的响应式编程模型,但是它在编译时生成高效的更新逻辑,而不是在运行时使用虚拟DOM。
- LitElement:LitElement是由Google Chrome团队开发的一种库,用于构建轻量级的Web组件。它使用了Web Components技术,并且不依赖于虚拟DOM,而是直接操作真实DOM,通过精细的变更追踪机制来更新DOM。
虚拟 DOM 开销来自哪里?
最明显的是,虚拟 DOM 的差异对比(diffing)不是免费的。
在将更改应用到真实 DOM 之前,你必须先比较新的虚拟 DOM 与之前的快照。以之前提到的水果列表示例为例,当你修改第一个 <li>
中的文本从 "Banana" 变为 "Pineapple" 时,虚拟 DOM 的 diff算法会经历以下关键步骤来更新列表:
- 根节点比较:发现
<ul>
根节点没有变化,继续比较子节点。 - 子节点比较:前两个
<li>
节点没有变化,因此不需要任何操作。 - 第三个
<li>
节点比较:发现文本内容从 "Banana" 变为 "Pineapple",标记这个节点需要更新。
在这三个步骤中,只有第三步在这种情况下有价值,因为——正如绝大多数更新的情况一样——应用程序的基本结构没有改变。如果我们能够直接跳到第三步,那将更加高效。
那么...虚拟 DOM 很慢吗?
并不完全是这样。更准确地说, “虚拟DOM通常足够快”,但有一定的限制条件。
现代框架 React、Vue 和其他虚拟 DOM 框架使用的 diffing 算法非常快。可以说,更大的开销在于组件本身。
React 最初的承诺是,你可以在每次状态变化时重新渲染整个应用,而不必担心性能问题。实际上,我认为这种说法并不准确。如果真是这样,那么就不会有像 shouldComponentUpdate
这样的优化需求了(这是一种告诉 React 何时可以安全跳过组件更新的方法)。
即使有了shouldComponentUpdate
,一次性更新整个应用的虚拟DOM也是一个庞大的工作。不久前,React 团队引入了一个叫做 React Fiber 的特性,它允许将更新分解为更小的块。这意味着(除其他外)更新不会长时间阻塞主线程,尽管它并没有减少总工作量或更新所需的时间。
Vue 3 同样也在虚拟 DOM 的性能方面进行了非常多的关键优化。Vue 3 的编译器执行了积极的优化,生成了提升性能的渲染函数代码,包括提升静态内容、留下运行时绑定类型的提示,以及最重要的是,将模板中的动态节点展平,以减少运行时遍历的成本。比如 Vue 3 引入了PatchFlag
,用于在编译时提供运行时提示,从而优化虚拟 DOM 的 patch 过程。
为什么需要虚拟 DOM ?
早期在单页应用(SPA) 流行之前,许多网站都是通过服务器端渲染的。这意味着对于每个用户交互或请求,服务器会发送一个完整的新页面回客户端,浏览器再进行渲染。这种模式也被称为多页应用(MPA) 。
对于 SPA,整个应用过程中通常只加载一个HTML文档,随后的所有用户交互和视图更新都是通过JavaScript在客户端动态完成的,不需要从服务器重新请求新页面。这是SPA的核心特点之一。
在SPA中,可能会涉及到大量的DOM操作,尤其是在复杂的项目中,可能会使用许多未优化的 DOM 操作。
假设我们想从数组中呈现列表。我们可以像下面这样进行。
function generateList(fruits) {
let ul = document.createElement('ul');
document.getElementByClassName('.fruits').appendChild(ul);
fruits.forEach(function (item) {
let li = document.createElement('li');
ul.appendChild(li);
li.innerHTML += item;
});
return ul
}
let fruits = ['Apple', 'Orange', 'Banana']
document.getElementById('#list').innerHtml = generateList(fruits)
现在如果列表发生变化,可以再次调用上述方法来生成列表。
fruits = ['Pineapple', 'Orange', 'Banana']
document.getElementById('#list').innerHtml = generateList(fruits)
在上面的代码中,生成了一个新列表并将其设置在文档中。这种方法的问题是,只有单个水果的文本发生了变化,但生成了一个新列表并更新到 DOM。此操作在 DOM 中很慢。我们可以像下面这样更改未优化的代码。这将减少 DOM 中的操作数。
document.querySelector('li').innerText = fruits[0]
未优化和优化后的代码最终结果是一样的,但未优化的DOM操作的代价是性能。如果列表的大小很大,你就能看见差异。这正是我们在早期框架如Backbone.js中遇到的问题。
因此,我们对于大问题的答案“我们为什么需要虚拟DOM?”是为了解决上述问题。
现代框架如 React 和 Vue 所做的是,每当状态/属性中有所变化时,就会创建一个新的虚拟DOM表示,并将其与前一个进行比较。在我们的示例中,唯一的变化将是“Apple”变为“Pineapple”。由于只是文本发生了变化,React 和 Vue 不会替换整个列表,而是通过以下代码更新DOM。
关于虚拟 DOM 的几点总结
虚拟 DOM 是现代前端框架如 React 和 Vue 的核心,它通过在内存中比较新旧UI状态并更新DOM,提供了一种高效的方式来实现声明式、状态驱动的UI开发。虚拟DOM的主要优点包括:
- 保证性能下限:虽然虚拟DOM的操作不一定是最优的,但它至少能保证在不需要手动优化的情况下,提供可接受的性能。
- 无需手动操作DOM:开发者可以专注于View-Model的逻辑,而框架会根据虚拟 DOM 和数据双向绑定自动更新视图,极大提高了开发效率。这意味着更少的 bug 代码,更多的时间可以花在创造性任务上,而不是繁琐的任务上。
- 跨平台能力:虚拟DOM作为JavaScript对象,可以方便地进行跨平台操作,如服务器渲染或Weex开发。
然而,虚拟DOM也有其局限性:
- 无法进行极致优化:在性能要求极高的应用中,虚拟DOM可能无法达到手动操作DOM所能达到的优化程度,如VScode等应用可能需要更直接的DOM操作来实现极端的性能优化。
虚拟DOM通过减少直接的DOM操作,简化了开发过程,但对于一些需要极致性能优化的场景,可能需要考虑其他方案或直接操作DOM。
参考
转载自:https://juejin.cn/post/7424901256135966746