前端有非常多的框架,React,Vue,Angular,Solid等,每个框架都有其独特的设计,这些设计或者惊艳,让人赞叹,或者坑爹,被人吐槽,但是无论如何这些设计都有其诞生的原因和背景,去看看这些设计的诞生,无论好坏,我们都有可以借鉴的地方,或许我们可以博采众长,创造一个汇集了所有优点的框架。
这是这个系列的第一篇文章。
什么是Virtual DOM
Virtual DOM,也就是虚拟DOM,React官网给出的定义是:
虚拟 DOM (VDOM) 是一个编程概念,在这个概念里,UI 以一种理想化的,或者说虚拟的表现形式被保存于内存中,并通过 ReactDOM 等库与"真实"DOM 同步。
更通俗一点,Virtual DOM,就是使用 JavaScript 对象来具体表示 DOM 信息和结构。
比如我们有这样一个HTML片段:
ini
<ul id='list'>
<li class='item'>Item 1</li>
<li class='item'>Item 2</li>
<li class='item'>Item 3</li>
</ul>
浏览器会为这个HTML生成一颗DOM树,我们可以使用提供的方法,来操作这棵DOM树,比如增加一个节点
javascript
let li = document.createElement("li");
li.setAttribute('class','item');
li.append('Item 4');
document.getElementById('list').append(li);
当我们需要增加10个节点的时候,我们就需要执行上面的逻辑10遍
而如果我们将这段HTML用JS来表述,比如用下面的结构
css
var element = {
tagName: 'ul',
props: {
id: 'list'
},
children: [
{tagName: 'li', props: {class: 'item'}, children: ["Item 1"]},
{tagName: 'li', props: {class: 'item'}, children: ["Item 2"]},
{tagName: 'li', props: {class: 'item'}, children: ["Item 3"]},
]
}
那么我们增加一个节点,只需要添加一个children
css
var element = {
tagName: 'ul',
props: {
id: 'list'
},
children: [
{tagName: 'li', props: {class: 'item'}, children: ["Item 1"]},
{tagName: 'li', props: {class: 'item'}, children: ["Item 2"]},
{tagName: 'li', props: {class: 'item'}, children: ["Item 3"]},
{tagName: 'li', props: {class: 'item'}, children: ["Item 4"]},
]
}
当我们短时间有多次操作时,我们就可以在这个JS中先把操作都做完,然后再根据变动的内容,再统一去改动真实的DOM,这样能有效减少浏览器的回流和重绘,以最少的代价渲染 DOM。
所以Virtual DOM 的本质是一个用来映射真实 DOM 的 JavaScript 对象,在用户和真实DOM之间架了一座桥,方便用户来操作DOM。
同时,也要明确Virtual DOM是一个概念,不是一种实现,所以同样是使用Virtual DOM,React和Vue的实现并不完全一致,当然,你也可以自己实现一个Virtual DOM的实现。
为什么会有Virtual DOM
在没有Virtual DOM之前,难道我们就无法操作DOM了吗,当然不是,无论浏览器还是jquery这类的库都为我们提供了比较方便的操作DOM的方法,那么我们为什么还需要Virtual DOM来帮我们操作DOM呢?
这就要回到提出Virtual DOM这个概念的React团队来了。
早在2010年,React诞生之前,Facebook (现在的meta)开发了 XHP 。XHP 是对 PHP 的语法拓展,它允许开发者直接在 PHP 中使用 HTML 标签,而不再使用字符串,并且允许自定义标签,使用起来就像这样
php
$list = <ul />;
$items = ...;
foreach ($items as $item) {
$list->appendChild(<li>{$item}</li>);
}
到了2013 年,Facebook (现在的meta)前端工程师 Jordan Walke 开始捣鼓React,受到XHP的启发,他想把把 XHP 的拓展功能迁移到 JS 中,也就是现在的JSX。
然而问题来了,操作DOM是个较为复杂的操作,尤其当你的DOM的结构非常复杂是,一旦你的DOM的结构进行变化,我操作DOM的逻辑也要更着变化,这块的维护过于复杂了,对于一些复杂的页面,我可能需要非常多的DOM操作,逻辑又多又杂,还容易出错。
为此 React 提出了一个新的思想,即始终整体"刷新"页面,不关心谁发生了变化,当发生前后状态变化时,React 会自动更新全部的UI,这让我们从复杂的 UI 操作中解放出来,使我们只需关于状态以及最终 UI 长什么样,这就是声明式编程。
当然显然这个是不太适合用于生产环境的,我就改一个文案,整个页面都刷新UI,虽然无脑的一波梭哈很爽,但是显然很慢,且完全没有必要。
为了解决上面说的问题,我们最好能做到增量更新,对于没有改变的 DOM 节点,让它保持原样不动,仅仅创建并替换变更过的 DOM 节点。所以,问题就来到了,两段JSX,我们怎么找出变更的节点。
第一个想法自然是对比DOM树了,然而DOM树在我们更新完DOM的时候才会变更,我们无法提前知道这个DOM树会变成什么样,那现成的DOM树无法使用,那就只能自己搞一个虚拟DOM树了,这个虚拟DOM树还比原生的DOM树简单,更能自定义,完美。
于是,在React中,渲染更新的过程变成了这样
- 维护一个使用 JS 对象表示的 Virtual DOM,与真实 DOM 一一对应
- 对前后两个 Virtual DOM 做 diff ,生成变更(Mutation)
- 把变更应用于真实 DOM,生成最新的真实 DOM
简单的说,Virtual DOM的出现,本身就是为了适应React这种声明式 UI框架,对于以前我们直接使用js操作DOM来讲,Virtual DOM没有什么意义,但是像React这种摒弃了直接操作 DOM 的细节,只关注数据的变动,数据再映射成UI的框架来讲,Virtual DOM能尽可能地减少真实DOM的操作。
React这类声明式框架,大幅度提升了代码的可读性和可维护性。而Virtual DOM和diff算法,则大幅度提升了React等框架的渲染和更新性能。
这也就是为什么你基本看到Virtual DOM都是和React,Vue框架绑定在一起的。
Virtual DOM的优点
很多文章和面试回答,都会告诉你直接操作DOM慢,使用Virtual DOM快,也有很多文章驳斥了这种观点,比如下面的文章:
我们要理解,Virtual DOM最终还是会映射成真实的DOM的操作,说白了,最终和你直接操作DOM没有任何区别,只不过一个是你手动编码操作,一个是框架自动diff并操作DOM,还多了Virtual DOM的构建和diff的操作,显而易见会比直接操作DOM慢。
我们认为Virtual DOM的快是针对UI全局刷新的方式来说的。直接操作DOM确实快,但是难以维护,编码的心智负担较大,使用React但是没有Virtual DOM,维护UI的心智负担小了,但是全局刷新造成的性能消耗大,React加上Virtual DOM,在保证减少开发人员维护UI的心智负担的同时以最小的代价来进行更新 DOM。
当然,Virtual DOM带来的好处也是显而易见的
- 最根本的一点,抽象化了DOM,所以可以基于这个抽象做很多事情,比如跨平台的能力,ReactNative,React VR 等
- 基于Virtual DOM,可以实现了对 DOM 的集中化操作,在数据改变时先对虚拟 DOM 进行修改,再反映到真实的 DOM 中,用最小的代价来更新 DOM,提高效率,解决了react这类框架数据驱动后所带来的性能问题的。
Virtual DOM的缺点
至于Virtual DOM的缺点,其实就是慢,毕竟是牺牲了部分性能换来的
- 渲染 DOM 时,由于多了一层虚拟 DOM 的计算,会比直接调用插入慢。
- 由于引入了额外的一层虚拟 DOM ,所以实际的内存占用也大了
当然相比Virtual DOM带来的性能和空间上的损耗,其优势会更明显一点。
可以借鉴的
自React 捣鼓出Virtual DOM以来,其实其他框架也在借鉴,比如Vue,引用尤大的话来说
Vue 2.0 引入 vdom 的主要原因是 vdom 把渲染过程抽象化了,从而使得组件的抽象能力也得到提升,并且可以适配 DOM 以外的渲染目标。
我们可以从Virtual DOM的诞生中得到一点启示
- 抽象化
将复杂的,不好描述的进行合理的抽象,结构化,基于抽象化,可以扩展得更多
- 取舍与权衡
所有框架,所有库不可能尽善尽美,一旦引入了框架,势必会比直接调用底层API多一点小号,但是这不代表不好,有时候我们需要做一点权衡,去牺牲一点其他相对不重要的,比提升一些更重要的,比如牺牲一部分性能,获取更好的开发体验。