虚拟 DOM 和 DOM diff

常常听说原生 DOM 操作比虚拟 DOM 操作慢,真的是这样吗?


先说结论:原生 DOM 操作和虚拟 DOM 操作相比,单次的速度是差不多的,并不会慢。但是原生 DOM 操作只会忠实地执行程序写定的内容,而不会自动优化;而虚拟 DOM 往往内置一些算法,能压缩 DOM 操作的次数,所以最后看起来,就是虚拟 DOM 操作整体上要快于原生 DOM 操作

那为什么会这样呢?首先要了解一下虚拟 DOM 到底是什么

虚拟 DOM 是什么

众所周知,在控制台可以看到 DOM 树,但是总不能每次在内存中操作,都分析一次页面结构吧?肯定要想办法保存到内存中

那么,内存中的 DOM 树就是虚拟 DOM 树,每个结点就是虚拟 DOM 结点

虚拟 DOM 的优点

在反应变更到页面上之前,我们可以通过延缓操作,来使得计算和变更分离,使得变更操作更集中

然后就可以计算,这一部分的变更中,有哪些是重复的呢?重复的变更,合并为同一次,就可以减少变更量了

比如添加 n 个结点,原本操作是for i in range(n): insert(node),现在是直接 insert(node*n),显然复杂度从 O(n) 降低到了 O(1)

同理,这一部分的 DOM 操作,涉及了多少结点呢?实际变更的结点,有没有指定的数量那么多?减少为需要的数量即可

比如变更 n 个结点数据,变 [0,1,1,1,...1,0][1,1,1,...,1],显然只有首尾两个需要变更,如果中间全部变更,那成本也太大了,所以这里也是复杂度从 O(n) 降低到了 O(1)

当然,实际的内部计算没有这么简单,只是举个简单例子,2333

除了上述两点外,大家都知道 js 是跨平台的,那么基于 js 的内存的 DOM 树,自然也是跨平台的,岂不美哉?

虚拟 DOM 的缺点

有优点自然也有缺点

用 c++ 写过树的朋友都知道,在 build 的时候每个结点都要 Node* p=new Node() 然后挂载到父结点的 lchild 或者 rchild 上

同理,在 js 创建虚拟 DOM,一般分为 vue 式和 react 式两种

vue 式

1
2
3
4
5
6
7
// 只有在 render 函数中才能得到 h 函数
h('div', {
class: 'red',
on: {
click: () => { }
},
}, [h('span',{},'span1'), h('span', {}, 'span2'])

创建出来的结点结构如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const vNode = {
tag: "div", // 标签名 or 组件名
data: {
class: "red", // 标签上的属性
on: {
click: () => {} // 事件
}
},
children: [ // 子元素们
{ tag: "span", ... },
{ tag: "span", ... }
],
...
}

react 式

1
2
3
4
5
createElement('div',{className:'red',onClick:()=> {}},[
createElement('span', {}, 'span1'),
createElement('span', {}, 'span2')
]
)

创建出来的结点结构如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const vNode = {
key: null,
props: {
children: [ // 子元素们
{ type: 'span', ... },
{ type: 'span', ... }
],
className: "red" // 标签上的属性
onClick: () => {} // 事件
},
ref: null,
type: "div", // 标签名 or 组件名
...
}

显然创建很麻烦,但是我们还有 template 和 jsx!

template

直接使用 XML 语法声明页面结构,然后通过 vue-loader 解析即可

jsx

1
2
3
4
5
6
7
8
()=>{
return (
<div className="red" onClick="{()=> {}}">
<span>span1</span>
<span>span2</span>
</div>
)
}

虽然简化了,但还是强依赖,且麻烦,不过总比不能创建好,2333

DOM diff 是什么

直接使用新结点和旧结点,找到最近公共祖先,然后比对该子树,哪里发生了变更

比如在先序序列 ABCDEFG,中序序列 CBDAFEG 的二叉树上,删除 F 结点,并在 F 的根上插入 H 结点

对于计算机来说,它得到的新子树是 EGH(先) GEH(中) 的子树,与原子树 EFG(先) FEG(中) 对比,是将结点 F 修改为结点 G,将结点 G 修改为结点 H

所以,人类认为的一次删除和一次操作,在计算机的理解就是两次修改

然后再同步到原树上,变为先序序列 ABCDEGH,中序序列 CBDAGEH

DOM diff 的优点

如上,可以不用遍历整棵树就快速算出新子树,且不需要全部替换树,只需要替换子树

DOM diff 的问题

如上,计算机的理解和人类的理解不一样

如果上例中没有第二步插入,那么计算机会理解为

  1. 变更结点 F 为结点 G
  2. 删除结点 G

要注意这一点!!但其实效率还是比你直接操作真实 DOM 要高


感谢阅读

--It's the end.Thanks for your read.--