Vue2虚拟DOM&diff算法(简易版)

$watchEvent

一、虚拟DOM简介

什么是虚拟DOM

虚拟DOM是一种优化性能的解决方案,当某个状态发生变化时,只更新与这个状态有关的DOM节点。其本质是一个JavaScript对象,内部保存了与虚拟DOM对应的真实DOM,子节点,选择器等属性。当然,虚拟DOM只是其中一个解决方案,如在Angular中就是通过脏检查的流程。

为什么引入虚拟DOM

1.ES和 DOM是两种东西,每次连接都需要消耗性能(由于浏览器通常将DOM和ECMAScript独立实现,因此JS每次去访问DOM都会消耗大量的性能)
2.操作DOM会导致重排和重绘,重排会占用、消耗CPU; 重绘会占用、消耗GPU
(重排:当DOM的变化影响了元素的几何属性(宽和高),浏览器需要重新计算元素的几何属性,同样其他相邻元素的几何属性和位置也会因此受到影响。浏览器会使渲染树中受到影响的部分失效,并重新构造渲染树。这个过程称为“重排”。
重绘:完成重排后,浏览器会重新绘制受影响的部分到屏幕中,该过程称为重绘.)
因此,我们需要尽可能减少访问DOM,虚拟DOM可以帮助我们减少对DOM的访问

二、diff算法

diff算法本质就是通过对比新旧的虚拟DOM来检查哪些节点需要更新/删除/增加等操作并访问DOM更新视图。

1.vnode

你可以将vnode理解为一个节点描述对象,他描述了怎样创建一个真实DOM节点 你可以将vnode理解成JavaScript对象版本的DOM元素
1
2
3
4
5
function vnode(sel, data, children, text, elm) {
//sel 选择器 data 节点的属性 children 子节点 text 文本 elm 真实DOM
let key = data.key
return { sel, data, children, text, elm, key }
}

2.patch

patch函数用来将vnode渲染成真正的DOM 通过对比新旧虚拟节点进而更新视图
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function patch(oldVnode, newVnode) {
//判断旧节点是真实还是虚拟节点 如果是真实节点 将其转换为虚拟节点 elm就是对应真实的dom节点
if (oldVnode.sel == '' || oldVnode.sel == undefined) {
oldVnode = vnode(oldVnode.tagName.toLocaleUpperCase(), {}, undefined, undefined, oldVnode)
}
//判断新旧节点是否是同一节点
if (oldVnode.sel == newVnode.sel && oldVnode.key == newVnode.key) {
//是同一节点 比较新旧节点
patchVnode(oldVnode,newVnode)
} else {
//不是同一节点 插入新的 删除旧的
let nodeElm = creatElement(newVnode)
oldVnode.elm.parentNode.insertBefore(nodeElm, oldVnode.elm);
oldVnode.elm.parentNode.removeChild(oldVnode.elm)
}
}

3.patchVnode

1
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
function patchVnode(oldVnode, newVnode) {
if (newVnode !== oldVnode) {
//新节点有text属性
if (newVnode.text) {
//新旧text不同
if (newVnode.text !== oldVnode.text) {
oldVnode.elm.innerText = newVnode.text
}
}
//新节点有children属性
if (newVnode.children) {
//旧节点是text
if (oldVnode.text) {
oldVnode.elm.innerHTML = ''
for (let i = 0; i < newVnode.children.length; i++) {
let dom = creatElement(newVnode.children[i])
oldVnode.elm.appendChild(dom)
}
}
//旧节点是children 最小量更新
if (oldVnode.children) {
newVnode.elm = creatElement(newVnode)
console.log(newVnode);
updateChildren(oldVnode.elm, oldVnode.children, newVnode.children)
}
}
}
}

4.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function h(sel, data, c) {
if (typeof c == 'string' || typeof c == 'number') {
//h('div',{},'文字')
return vnode(sel, data, undefined, c, undefined)
} else if (Array.isArray(c)) {
//h('div',{},{h(),h()})
let children = []
for (let i = 0; i < c.length; i++) {
if (!c[i].hasOwnProperty('sel')) {
throw new Error('第' + i + '个数据中应存在sel属性')
} else {
children.push(c[i])
}
}
return vnode(sel, data, children, undefined, undefined)
} else if (typeof c == 'object' && c.hasOwnProperty('sel')) {
//h('div',{},h())
let children = [c]
return vnode(sel, data, children, undefined, undefined)
}
}

5.creatElement

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function creatElement(v_node) {
let domNode = document.createElement(v_node.sel)
//传入的是文字
if(v_node.text !== '' && (v_node.children == undefined || v_node.children.length == 0)){
domNode.innerText = v_node.text
}else if(v_node.children.length != 0){
//传入的是子节点的数组 遍历该数组 将数组的dom对象添加到父节点中
let arr = v_node.children
for(let i = 0;i<arr.length;i++){
let nodeElm = creatElement(arr[i])
domNode.appendChild(nodeElm)
}
}
v_node.elm = domNode
return v_node.elm
}

6.updateChildren

1
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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
function checkSameNode(a, b) {
if (a.sel == b.sel && a.key == b.key) {
//console.log(a.elm, b.elm);
}
return a.sel == b.sel && a.key == b.key
}
function updateChildren(parentElm, oldC, newC) {

let newStartIdx = 0

let oldStartIdx = 0

let newEndIdx = newC.length - 1

let oldEndIdx = oldC.length - 1


let newStartVnode = newC[newStartIdx]

let oldStartVnode = oldC[oldStartIdx]

let newEndVnode = newC[newEndIdx]

let oldEndVnode = oldC[oldEndIdx]

let keyMap = null;

while (newStartIdx <= newEndIdx && oldStartIdx <= oldEndIdx) {
console.log(newStartIdx, newEndIdx, oldStartIdx, oldEndIdx);
if (checkSameNode(oldStartVnode, newStartVnode)) {
//新前 旧前
console.log('新前 旧前')
patchVnode(oldStartVnode, newStartVnode)
oldStartVnode = oldC[++oldStartIdx]
newStartVnode = newC[++newStartIdx]
}
else if (checkSameNode(oldEndVnode, newEndVnode)) {
//新后 旧后
console.log('新后 旧后');
patchVnode(oldEndVnode, newEndVnode)
oldEndVnode = oldC[--oldEndIdx]
newEndVnode = newC[--newEndIdx]
}
else if (checkSameNode(oldStartVnode, newEndVnode)) {
//新后 旧前
console.log('新后 旧前');
//如果节点上的数据一致需要移动节点 插入到所有未处理节点之前
parentElm.insertBefore(oldStartVnode.elm, oldEndVnode.elm);
patchVnode(oldStartVnode, newEndVnode)
oldStartVnode = oldC[++oldStartIdx]
newEndVnode = newC[--newEndIdx]

}
else if (checkSameNode(oldEndVnode, newStartVnode)) {
//新前 旧后
console.log('新前 旧后');
parentElm.insertBefore(oldEndVnode.elm, oldStartVnode.elm)
patchVnode(oldEndVnode, newStartVnode)
oldEndVnode = oldC[--oldEndIdx]
newStartVnode = newC[++newStartIdx]
} else {
//都没有命中
if (!keyMap) {
keyMap = {}
for (let i = oldStartIdx; i < oldEndIdx; i++) {
key = oldC[i].key
keyMap[key] = i
}
}
let keyOldIdx = keyMap[newStartVnode.key]
if(keyOldIdx == undefined){
//全新的项
console.log('全新的项');
parentElm.insertBefore(newStartVnode.elm,oldStartVnode.elm);
}else{
//在旧节点中存在的项 需要移动到新的位置
patchVnode(oldC[keyOldIdx],newStartVnode)
parentElm.insertBefore(oldC[keyOldIdx].elm,oldStartVnode.elm);
console.log('在旧节点中存在的项 需要移动到新的位置');
}
newStartVnode = newC[++newStartIdx]
}
}
if (newStartIdx <= newEndIdx) {
console.log('仍有新节点未处理');
while (newStartIdx <= newEndIdx) {
parentElm.insertBefore(newEndVnode.elm,oldStartVnode.elm)
newEndVnode = newC[--newEndIdx]
}
}
if (oldStartIdx <= oldEndIdx) {
console.log('仍有旧节点未处理');
while (oldStartIdx <= oldEndIdx) {
console.log(oldEndVnode);
parentElm.removeChild(oldEndVnode.elm)
oldEndVnode = oldC[--oldEndIdx]
}
}
}

Vue2虚拟DOM&diff算法(简易版)
https://jing-jiu.github.io/jing-jiu/2021/06/05/Framework/Vue2/虚拟DOM&diff算法/
作者
Jing-Jiu
发布于
2021年6月5日
许可协议