虚拟 DOM 和 DOM diff

2,780 阅读5分钟

什么是虚拟 DOM ?

Virtual dom,即虚拟DOM节点,指的是使用纯js对象来表示一个节点的结构,该对象包含了真实DOM的结构与属性,通过对比新旧两个虚拟DOM,找出差异,然后修改旧的虚拟DOM(可以认为真实的DOM是基于旧的DOM来渲染的)。它通过JS的Object对象模拟DOM中的节点,然后再通过特定的render方法将其渲染成真实的DOM节点。

很多时候我们都说 DOM 操作很慢,虚拟 DOM 操作快,但是这需要有对比概念,DOM 操作慢是对比与 JS 原生 API,如数组操作,其实在很多时候,那些基于 DOM 的库(Vue/React)并没有在操作 DOM 时比 DOM 快,只是在某些情况下虚拟 DOM 更快。

简单点,虚拟 DOM 其实就是一个能够代表 DOM 数的对象,通常含有标签名、标签名上的属性、事件监听、子元素们以及其他属性。在 React 中,我们可以通过 JSX 来方便地创建虚拟 DOM,在 Vue 中,我们可以通过 template 模板语法来形象地创建虚拟 DOM ,如下所示:

在 React 中:

<div className = "red" onClick = {fn}>
    <span>span1</span>
    <span>span1</span>
</div>

在 Vue 中:

<div className = "red" @click = "fn">
    <span>span1</span>
    <span>span1</span>
</div>

虚拟DOM的优点

1. 减少DOM操作

由于真实的DOM是在渲染引擎生成的,而通过JS来操作DOM则涉及到了跨线程的操作,速度会相比同线程操作来的慢,但是现代的网页更强调可交互性,所以操作DOM还是不可避免,那么我们可以减少操作DOM的次数,来提高性能。

方法一:虚拟 DOM 可以将多次 DOM 操作合并为一次操作

比如你有 1000 个 DOM 节点,通过浏览器提供的 DOM API 可能需要操作 1000 次,但是虚拟 DOM 可以合起来只操作 1 次。

方法二:DOM Diff 算法可以摒除多余的 DOM 操作

DOM Diff 算法,顾名思义就是看对比前后两次 DOM 树的区别,然后只去更新有变化的 DOM 节点。 但是,一旦DOM节点数非常高,DOM API 反而会快于虚拟 DOM,因为虚拟 DOM 自身存在大量计算。

2. 虚拟 DOM 可以跨平台

虚拟 DOM 不仅可以变成 DOM,还可以变成小程序、iOS 应用、安卓应用,这是因为虚拟 DOM 本质上只是一个 JS 对象,所以可以进行跨平台。

虚拟DOM的缺点

虚拟 DOM的缺点就是需要额外的创建函数,在 React 中需要使用 create Element,在 Vue 中则需要使用 h 函数,但是我们可以通过 JSX 来将其简化为 XML 的写法。

在极限情况下,例如通过虚拟 DOM 添加十万个节点(当然这种情况比较少见,一般一个页面的节点在两千到一万之间),原生 DOM 和 Vue 大概需要 10s,而 React 则需要 30s。如果你在使用 React 时要添加大量节点,建议直接使用虚拟 DOM 而不是 createElement 来提升效率。但若只是添加一些节点的话使用 createElement 反而可能会更快一些。这样虚拟DOM就十分依赖打包工具了,没有打包工具进行转译的话,代码也就不会顺利运行了。

什么是 DOM diff ?

谈到虚拟 DOM 的优点时提到可以借助 DOM diff 将多余的操作省掉,那么 DOM diff 是什么呢?这是虚拟 DOM 的一种对比算法,当数据发生变化时,它会去对比前后的 DOM ,查找哪些地方发生了变化,然后返回需要改变的操作,其实也就是一个函数,通常称之为 patch,也可以理解成Dom diff 通过JS层面的计算,返回一个patch对象,即补丁对象,在通过特定的操作解析patch对象,完成页面的重新渲染。其中可能的大概逻辑是:

1.首先是做 Tree diff,将新旧两棵树逐层对比,找出哪些节点需要更新,如果节点是组件就看 Component diff,如果节点是标签就看 Element diff。
2.在Component diff中,如果节点是组件,就先看组件类型,类型不同直接替换(删除旧的),类型相同则只更新属性,然后深入组件做 Tree diff(递归)。
3.在Element diff中,如果节点是原生标签,则看标签名,标签名不同直接替换,相同则只更新属性,然后进入标签后代做 Tree diff(递归)。

DOM diff 的特点

从上面的描述中可知,通过使用 DOM diff,我们可以只更新发生了改变的节点,而不必每次更新所有的节点,但是 DOM diff 也是存在一定问题的,那就是在同级节点中对比会有一些 bug,会出现识别错误的问题,因为计算机总是会从左到右、从上到下进行计算,假设现在有三个同级节点,当我们删除了第二个节点后,在 DOM diff 算法中,其实并不是认为删除了第二个节点,而是认为原本的第二个节点发生了改变,变成了第三个节点的内容,而第三个节点则被认为是删除了,那么怎么避免这个问题呢?这就需要给每一个节点加上一个唯一的属性 key,来标识每个节点,这要就可以避免识别错误,也就是为什么在 vue 中的 v-for 里必须要声明 :key。