虚拟DOM和diff算法
在使用 Vue 框架之前,我们需要用 JS 或者 jQuery 直接操作 DOM 从而进行视图的更新,要知道如何有多个数据发生变化的话,要直接操作 DOM 进行视图的更新会占用很多开销。而在 Vue 框架中,我们知道通过 data()中数据的改变进行视图的更新。其中具体的过程其实是:数据发生改变 --> 虚拟 DOM 计算变更 --> 操作真实 DOM --> 视图更新。
# 虚拟 DOM
虚拟 DOM 指的是用 JS 对象来模拟 DOM 结构,这个对象由三个重要属性:tag、props 和 children,分别对应 DOM 的标签、属性、子元素。我们以下面的 DOM 结构来书写一个虚拟 DOM,也就是 JS 对象:
<div id="id" class="container">
<h1>虚拟DOM</h1>
<ul style="color:orange">
<li>第一项</li>
<li>第二项</li>
<li>第三项</li>
</ul>
</div>
2
3
4
5
6
7
8
对应的虚拟 DOM 对象为:
{
tag: 'div',
props: {
id: 'app',
className: 'container'
},
children: [
{
tag: 'h1',
children: '虚拟DOM'
},
{
tag: 'ul',
props: { style: 'color:orange'},
children: [
{
tag: 'li',
children: '第一项'
},
{
tag: 'li',
children: '第二项'
},
{
tag: 'li',
children: '第三项'
}
]
}
]
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
# 虚拟 DOM 的好处
那为什么要用虚拟 DOM 呢,显而易见虚拟 DOM 实际上是 JS 对象,相比于直接操作 DOM 来更新视图,直接使用 JS 引擎操作 JS 对象会更快。此外,操作虚拟 DOM 更新视图,它不会将整个 DOM 节点重新渲染,只会渲染改变的 DOM 节点。
我们通过一个例子就可以得出很直观地看出虚拟 DOM 的优点:
# diff 算法
diff 算法是虚拟 DOM 的核心。 我们用虚拟 DOM 来表示真实 DOM,当数据发生改变时,会产生一个新的虚拟 DOM,通过对比新旧虚拟 DOM 的最小变化,从而更新视图。diff 算法则是用来查找新旧虚拟 DOM 的差异,它的步骤是:
- 遍历老虚拟 DOM
- 遍历新虚拟 DOM
- 重新排序
如果直接这样遍历比较的话,复杂度是 O(n^3),根据 DOM 树结构特点,对 diff 算法改进:
- 只比较同一层级,不跨级比较,复杂度则变为 O(n);
- 标签名不同,直接删除,不继续深度比较;
- 标签名相同,key 相同,就认为是相同节点,不继续深度比较;(v-for 绑定的:key)
# 虚拟 DOM 和 diff 流程
# 生成 VNode
vnode函数传入sel,data,children,text,undefined, 返回节点对象{sel, data, children, text, elm, key }
注意: children 和 text 只能存在一个;
elm 是虚拟节点sel 对应的真实的DOM节点, 在使用patch函数时使用,代表要渲染到哪一个真实DOM元素上;
key 是v-for 绑定的key, 其实在Vue每个组件都是可以接收key的,只不过v-for是必须提供key值。(为什么需要提供呢,在后面的函数中有提到)
# patch 函数
在首次页面渲染的时候执行一次patch函数,将VNode渲染到空的容器中:patch(container, vnode)
;这里container是真实DOM的节点。
之后页面更新时将新的VNode替换旧的VNode:patch(vnode, newVnode)
patch函数解析:
首先判断第一个参数是否为Vnode类型,如果不是则创建一个空的vnode,并关联到对应的DOM元素;
再通过sameVnode判断新旧vnode是否相同,比较的两个vnode的key和sel是否相等,如果相同的话就直接使用patchVnode更新;
如果不相同的话,说明两个vnode的key或tag不相同,是不同的Vnode,则使用createElm来创建一个真实的DOM元素,并插入新的DOM删除旧的DOM。
# patchVnode 函数
在新旧Vnode相同时候,就用patchNode进行更新。
首先将旧的vnode关联的DOM元素传递给新的vnode;
再判断新旧vnode的children是否相等,如果相等就直接返回;如果不相等的话,分为以下几种情况:
- 新的vnode 有text属性,无children属性,且新旧vnode的text属性不相同时
如果旧的vnode 有children属性,则移除旧的vnode的children属性移除;并且设置新的vnode的text
- 新的vnode 无text
①新旧vnode的children都有定义,则利用updateChildren
对children进行更新;
②新的vnode有children,而旧的没有children,则清空旧节点的text,并新的children添加;
③新的vnode 无children,旧的有children,则移除旧的vnode;
④旧的vnode有text,则清空text。
# updateChildren 函数
这个是diff最小量更新的核心,通过四个指针来比较,分别是老的children的oldStartIdx、oldEndIdx,新children的newStartIdx、newEndIdx。
一共有四个对比情况,看是否vnode相同:
- 老的开始和新的开始对比
如果两者vnode相同,则进行patchVnode操作,又回去了进行节点下面的比较,相当于递归;然后对于指针进行移动。
老的结束和新的结束对比
老的开始和新的结束对比
老的结束和新的结束对比
如果上面四种情况都没有命中,则拿新的开始的key,在老的children里面去找有没有某个节点有对应的key:
如果在老的children中找到了:再判断sel是否相等,如果不相等,则创建新的元素,否则进行patchVnode对节点进行更新;
如果没有找到,则创建新的元素。