Virtual DOM
是一棵以JavaScript
对象作为基础的树,每一个节点可以将其称为VNode
,用对象属性来描述节点,实际上它是一层对真实DOM
的抽象,最终可以通过渲染操作使这棵树映射到真实环境上,简单来说Virtual DOM
就是一个Js
对象,是更加轻量级的对DOM
的描述,用以表示整个文档。
在浏览器中构建页面时需要使用DOM
节点描述整个文档。
<div class="root" name="root">
<p>1</p>
<div>11</div>
</div>
如果使用Js
对象去描述上述的节点以及文档,那么便类似于下面的样子。
{
type: "tag",
tagName: "div",
attr: {
className: "root"
name: "root"
},
parent: null,
children: [{
type: "tag",
tagName: "p",
attr: {},
parent: {} /* 父节点的引用 */,
children: [{
type: "text",
tagName: "text",
parent: {} /* 父节点的引用 */,
content: "1"
}]
},{
type: "tag",
tagName: "div",
attr: {},
parent: {} /* 父节点的引用 */,
children: [{
type: "text",
tagName: "text",
parent: {} /* 父节点的引用 */,
content: "11"
}]
}]
}
Virtual DOM
是一种编程概念,在这个概念里,UI
以一种理想化的,或者说虚拟的表现形式被保存于内存中,并通过如ReactDOM
等类库使之与真实的DOM
同步,这一过程叫做协调。这种方式赋予了React
声明式的API
,您告诉React
希望让UI
是什么状态,React
就确保DOM
匹配该状态,这样可以从属性操作、事件处理和手动DOM
更新这些在构建应用程序时必要的操作中解放出来。
与其将Virtual DOM
视为一种技术,不如说它是一种模式,人们提到它时经常是要表达不同的东西。在React
的世界里,术语Virtual DOM
通常与React
元素关联在一起,因为它们都是代表了用户界面的对象,而React
也使用一个名为fibers
的内部对象来存放组件树的附加信息,上述二者也被认为是React
中Virtual DOM
实现的一部分。
在之前,Facebook
是PHP
大户,所以React
最开始的灵感就来自于PHP
。
在2004
年这个时候,大家都还在用PHP
的字符串拼接来开发网站。
$str = "<ul>";
foreach ($talks as $talk) {
$str += "<li>" . $talk->name . "</li>";
}
$str += "</ul>";
这种方式代码写出来不好看不说,还容易造成XSS
等安全问题。应对方法是对用户的任何输入都进行转义Escape
,但是如果对字符串进行多次转义,那么反转义的次数也必须是相同的,否则会无法得到原内容,如果又不小心把HTML
标签给转义了,那么HTML
标签会直接显示给用户,从而导致很差的用户体验。
到了2010
年,为了更加高效的编码,同时也避免转义HTML
标签的错误,Facebook
开发了XHP
。XHP
是对PHP
的语法拓展,它允许开发者直接在PHP
中使用HTML
标签,而不再使用字符串。
$content = <ul />;
foreach ($talks as $talk) {
$content->appendChild(<li>{$talk->name}</li>);
}
这样的话,所有HTML
标签都使用不同于PHP
的语法,我们可以轻易的分辨哪些需要转义哪些不需要转义。不久的后来,Facebook
的工程师又发现他们还可以创建自定义标签,而且通过组合自定义标签有助于构建大型应用。
到了2013
年,前端工程师Jordan Walke
向他的经理提出了一个大胆的想法:把XHP
的拓展功能迁移到Js
中,首要任务是需要一个拓展来让JS
支持XML
语法,该拓展称为JSX
。因为当时由于Node.js
在Facebook
已经有很多实践,所以很快就实现了JSX
。
const content = (
<TalkList>
{talks.map(talk => <Talk talk={talk} />)}
</TalkList>
);
在这个时候,就有另外一个很棘手的问题,那就是在进行更新的时候,需要去操作DOM
,传统 DOM API
细节太多,操作复杂,所以就很容易出现Bug
,而且代码难以维护。然后就想到了PHP
时代的更新机制,每当有数据改变时,只需要跳到一个由PHP
全新渲染的新页面即可。
从开发者的角度来看的话,这种方式开发应用是非常简单的,因为它不需要担心变更,且界面上用户数据改变时所有内容都是同步的。为此React
提出了一个新的思想,即始终整体刷新页面,当发生前后状态变化时,React
会自动更新UI
,让我们从复杂的UI
操作中解放出来,使我们只需关于状态以及最终UI
长什么样。这个时候,我只需要关系我的状态(数据是什么),以及UI
长什么样(布局),不再需要关系操作细节。
这种方式虽然简单粗暴,但是很明显的缺点,就是很慢。另外还有一个问题就是这样无法包含节点的状态,比如它会失去当前聚焦的元素和光标,以及文本选择和页面滚动位置,这些都是页面的当前状态。
为了解决上面说的问题,对于没有改变的DOM
节点,让它保持原样不动,仅仅创建并替换变更过的DOM
节点,这种方式实现了DOM
节点复用Reuse
。至此,只要能够识别出哪些节点改变了,那么就可以实现对DOM
的更新,于是问题就转化为如何比对两个DOM
的差异。说到对比差异,可能很容易想到版本控制git
。DOM
是树形结构,所以diff
算法必须是针对树形结构的,目前已知的完整树形结构的编辑距离diff
算法复杂度为O(n^3)
。但是时间复杂度O(n^3)
太高了,所以Facebook
工程师考虑到组件的特殊情况,进行了一些优化与折中,然后将复杂度降低到了O(n)
。
DOM
是复杂的,对它的操作尤其是查询和创建是非常慢非常耗费资源的。看下面的例子,仅创建一个空白的div
,其实例属性就达到294
个。
// Chrome v84
const div = document.createElement("div");
let m = 0;
for (let k in div) m++;
console.log(m); // 294
对于DOM
这么多属性,其实大部分属性对于做Diff
是没有任何用处的,所以如果用更轻量级的Js
对象来代替复杂的DOM
节点,然后把对DOM
的diff
操作转移到Js
对象,就可以避免大量对DOM
的查询操作。这个更轻量级的Js
对象就称为Virtual DOM
。那么现在的过程就是这样:
- 维护一个使用
Js
对象表示的Virtual DOM
,与真实DOM
一一对应。 - 对前后两个
Virtual DOM
做diff
,生成变更Mutation
。 - 把变更应用于真实
DOM
,生成最新的真实DOM
。
可以看出,因为要把变更应用到真实DOM
上,所以还是避免不了要直接操作DOM
,但是React
的diff
算法会把DOM
改动次数降到最低。关于React
中的虚拟DOM
创建过程可以参考https://github.com/facebook/react/blob/9198a5cec0936a21a5ba194a22fcbac03eba5d1d/packages/react/src/ReactElement.js#L348
。
传统前端的编程方式是命令式的,直接操纵DOM
,告诉浏览器该怎么干,这样的问题就是,大量的代码被用于操作DOM
元素,且代码可读性差,可维护性低。React
的出现,将命令式变成了声明式,摒弃了直接操作DOM
的细节,只关注数据的变动,DOM
操作由框架来完成,从而大幅度提升了代码的可读性和可维护性。
在初期我们可以看到,数据的变动导致整个页面的刷新,这种效率很低,因为可能是局部的数据变化,但是要刷新整个页面,造成了不必要的开销。所以就有了Diff
过程,将数据变动前后的DOM
结构先进行比较,找出两者的不同处,然后再对不同之处进行更新渲染。但是由于整个DOM
结构又太大,所以采用了更轻量级的对DOM
的描述—虚拟DOM
。
不过需要注意的是,虚拟DOM
和Diff
算法的出现是为了解决由命令式编程转变为声明式编程、数据驱动后所带来的性能问题的。换句话说,直接操作DOM
的性能并不会低于虚拟DOM
和Diff
算法,甚至还会优于。框架的意义在于为你掩盖底层的DOM
操作,让你用更声明式的方式来描述你的目的,从而让你的代码更容易维护,没有任何框架可以比纯手动的优化DOM
操作更快,因为框架的DOM
操作层需要应对任何上层API
可能产生的操作,它的实现必须是普适的。
Virtual DOM
在牺牲(牺牲很关键)部分性能的前提下,增加了可维护性,这也是很多框架的通性。- 实现了对
DOM
的集中化操作,在数据改变时先对虚拟DOM
进行修改,再反映到真实的DOM
,用最小的代价来更新DOM
,提高效率。 - 打开了函数式
UI
编程的大门。 - 可以渲染到
DOM
以外的端,使得框架跨平台,比如ReactNative
,React VR
等。 - 可以更好的实现
SSR
,同构渲染等。 - 组件的高度抽象化。
- 首次渲染大量
DOM
时,由于多了一层虚拟DOM
的计算,会比innerHTML
插入慢。 - 虚拟
DOM
需要在内存中的维护一份DOM
的副本,多占用了部分内存。 - 如果虚拟
DOM
大量更改,这是合适的。但是单一的、频繁的更新的话,虚拟DOM
将会花费更多的时间处理计算的工作。所以如果你有一个DOM
节点相对较少页面,用虚拟DOM
,它实际上有可能会更慢,但对于大多数单页面应用,这应该都会更快。
https://github.com/WindrunnerMax/EveryDay
https://zhuanlan.zhihu.com/p/99973075
https://www.jianshu.com/p/e0a3ac85db5c
https://www.jianshu.com/p/9a1d2750457f
https://github.com/livoras/blog/issues/13
https://juejin.cn/post/6844904165026562056
https://juejin.cn/post/6844903640512086029
https://zh-hans.reactjs.org/docs/faq-internals.html#what-is-the-virtual-dom