1、虚拟 DOM
虚拟 DOM 是相对浏览器渲染出来的真实DOM而言的。虚拟DOM是一个 JS 对象,一个能代表DOM树的对象,通常含有标签名、标签上的属性、事件监听和子元素等。
以 Vue 的虚拟DOM为例:
const vNode = {
tag: "div", // 标签名 or 组件名
data: {
class: "red", // 标签上的属性
on: {
click: () => {} // 事件
}
},
children: [ // 子元素们
{ tag: "span", ... },
],
...
}
在上面Vue的虚拟DOM例子里:tag用来表示标签名或组件名;class用来表示标签上的类;children来表示标签上的子元素。同理,在React的虚拟DOM里也有类似的描述真实DOM结构的属性。
1.1 虚拟DOM的优点
虚拟DOM与直接操作真实DOM相比有以下两个优点:
- 减少了DOM操作。 虚拟DOM可以将多次操作合并为一次操作。比如虚拟DOM添加1000个节点,可以一次就添加进入真实DOM内;虚拟DOM借助 DOM diff 可以把多余的操作省掉。比如添加1000个节点,其实只有10个是新增的,那只需要进行新增10个节点的操作即可。
- 跨平台。 由于虚拟DOM本质上只是一个 JS 对象,并不依赖真实平台环境,所以也可以应用在小程序、iOS应用、安卓应用等。
1.2 虚拟DOM的缺点
在创建虚拟DOM时,React需要通过createElement函数创建虚拟DOM,后来 React 使用了JSX语法来优化虚拟DOM的使用。 React创建虚拟DOM的方法改变如下:
//原来 React createElement
createElement('div',{className:'red',onClick:()=> {}},[
createElement('span', {}, 'span1'),
createElement('span', {}, 'span2')
]
)
//React 使用了JSX语法后
<div className="red" onClick={fn}>
<span>span1</span>
<span>span2</span>
</div>
//但是需要通过 babel 转为 createElement 形式
如上例所示,React 现在创建虚拟DOM是使用 JSX语法,并通过 babel 转为 createElement 形式。
而Vue则需要通过 render函数里得到的 h 来创建虚拟DOM,也可以简化通过采用 vue-loader 来转为h形式。如下例:
// Vue 直接通过render函数里得到的 h
h('div', {
class: 'red',
on: {
click: () => { }
},
}, [h('span',{},'span1'), h('span', {}, 'span2'])
//简化,采用 vue-loader 来转为h形式
//Vue Template
<div class="red" @click="fn">
<span>span1</span>
<span>span2</span>
</div>
原来创建虚拟DOM需要额外的函数:createElement 或 h;现在简化后可以通过 JSX 语法或 vue-loader来创建虚拟DOM,但是后者也有缺点,因为使用了JSX 语法或 vue-loader,使得虚拟DOM的创建又需要依赖打包了。
2、DOM diff
DOM diff从字面上可以理解就是比较两个DOM树的差异。
DOM diff 在做比较时分为了tree diff、component diff、element diff三个层级,分别描述三个层级大概的更新逻辑:
-
Tree diff 逐层对比新旧两课树,找出需要更新的节点。如果节点是组件就看 component diff ;如果节点是标签就看 element diff 。
-
component diff
- 如果需要更新的节点是组件,就先观察组件类型;
- 类型不同就直接替换,删除旧节点,后装载新节点。
- 节点类型一样,仅仅是属性或者属性值变了。不会触发删除节点,只是会更新属性。
- 最后深入组件再次进行 tree diff,逐层比较更深处的节点。
- element diff
- 如果节点是原生标签,观察标签名;
- 标签名不同,直接替换;
- 标签名相同(即元素相同),就进一步比较更新属性。
- 最后进入标签再次 tree diff,逐层比较更深处的节点。
3、DOM diff 优点和问题
关于 DOM diff 的优点,前面讲到了虚拟DOM减少DOM的操作的两种情况:
- 虚拟DOM可以将多次操作合并为一次操作;
- 虚拟DOM借助 DOM diff 可以省掉多余的操作。 以上的两种情况都需要通过 DOM diff 来对DOM树进行比较,记录之间存在的差异,然后再应用到真实的DOM上。
diff 的问题和 key
DOM diff 有时候会因为无法正确识别节点而进行多余的操作,
如下有A、B、C三个节点,希望在B、C节点之间插入一个E节点;
但是 Diff算法默认执行起来是A、B不变,C更新为E,最后插入C。
为了更高效的更新虚拟DOM,可以在 React/Vue 通过 key 来为每一个节点做一个唯一标识。比如说,上述的例子中如果为节点都设置了key ,DOM diff就能判断出来是新增了E节点,从而可以省略C更新E的操作。
使用 key 还可以管理可复用的元素。比如在Vue中如果有两个相同的元素,只需为两个标签添加一个具有唯一值的 key 即可表达出“这两个元素相互独立”。
注意:不要使用 index 作为key。因为 index 是连续的,以上面A、B、C节点为例,如果删除B节点,index会从1、2、3变成 1 2,被删除的会是C而不是B。