# 虚拟 DOM 和 Diff 算法

# 虚拟 DOM

虚拟 DOM 就是用来将真实 dom 抽象为 Js 对象的形式

工作原理就是当页面内容发生改变时回生成一个新的 DOM 树与旧的 DOM 树进行比较

找出差异后以最小的更新去进行增删改

优势:

  1. 性能提升:会尽量减少对 DOM 的操作,从而减少操作 DOM 的次数,从而提高性能
  2. 跨平台:因为是抽象的对象所以可以在任何支持 Javascript 的平台上运行

# Diff 算法

Vue 的 diff 算法就离不开 Snabbdom;

Snabbdom 是一个虚拟 DOM 库,高效的,简单的;

Vue2 的 Vnode 就是借鉴这个库来编写的,Vue3 的 Diff 就更加强大了,颗粒度更精确,相对于 Vue2 性能也提升了很多。

Vue2 的新老节点替换规则(执行流程):

1. 对比新旧 DOM 的根节点是否相同,不相同则直接替换整个旧的 DOM 树,相等则下一步

2. 接下来 Vue 会遍历新旧虚拟 DOM 的子节点,比较节点的标签名是否相同

不同:替换整个节点及其子节点

相同:则下一步

3. 接下来,比较节点的属性,

不同则更新该属性

有新属性则插入新属性,

旧有新没有则移除旧的属性

4. 接下载比较节点的子节点

比较数量是否相同,不同则替换整个子节点

相同则逐个比较子节点

5. 比较子节点时 Vue 会采用两个指针的方式,分别从旧节点的首位开始遍历,相同则下一步,

不相同则比较上下节点是否有相同的:

有则将新节点移动到正确的位置,

没有则创建新节点,插入到旧节点的位置

# 简单手写代码解析

1. 搭建 webpack 环境

1
npm init -y

2. 引入 webpack 本地服务

1
npm i webpack@4 webpack-cli@3 webpack-dev-erver@3 -S

3. 配置入口出口文件 webpack.json

1
2
3
4
5
6
7
8
9
10
11
12
13
module.exports = {
entry: {
index: './src/index.js'
},
output: {
path: __dirname + "/public",
filename: './js/[name].js'
},
devServer: {
contentBase: './public',
inline: true
}
}

4. 配置相关文件

1
2
3
4
1.public/index.html 单页面文件
2.src/index.js 入口文件
根据格式在1中引入index.js (filename: './js/[name].js')
3.src/dom //用来存放手写的h函数,patch函数等

5.index.js 编写简单的虚拟 DOM 用来测试手写的 diff 算法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import { h } from './dom/h'//引入h函数
import patch from './dom/patch'//引入patch函数
const container = document.getElementById('container')
const btn = document.getElementById('btn')
let vnode = h('h1', {}, '来个好工作')
let vnode0 = h('ul', {}, [
h('li', { key: 8 }, '月入5W'),
h('li', { key: 7 }, [h('div', {}, '唱大戏'), h('div', {}, '拉大锯')]),
h('li', { key: 6 }, '吹拉弹唱'),
h('li', { key: 5 }, '样样精通'),
])
let vnode1 = h('ul', {}, [
h('li', { key: 8 }, '月入5W'),
h('li', { key: 7 }, [h('ul', {}, '唱大戏'), h('ul', {}, 'c')]),
h('li', { key: 6 }, '6'),
h('li', { key: 5 }, 'c'),
])

6. 手写 h 函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import vnode from './vnode'//引入转换虚拟DOM的函数
//写一个简单的H函数
export const h = function (sel, data, params) {
// 判断第三个参数等于string/array//判断有没有子节点,没有直接返回子节点为空
if (typeof params == "string") {
//string时直接返回
return vnode(sel, data, undefined, params, undefined);
} else if (Array.isArray(params)) {
//数组编译后返回
let children = []
for (let v of params) {
children.push(v)
}
//将children遍历后返回虚拟DOM
return vnode(sel, data, children, undefined, undefined);

}
}

Vnode.js

1
2
3
4
5
6
export default function (sel, data, children, params, elm) {
return {
sel, data, children, params, elm
}
}
//params是text文本内容

7. 手写 patch 函数涉及 DOM 创建与删除等

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import vnode from './vnode'//虚拟节点转换
import createElm from './createEle';//创建节点函数
import patchVnode from './patchVnode.js'//判断是不是相同标签类型,是的话层层对比
export default function (oldVnode, newVnode) {
//判断是不是虚拟节点,不是的话转换为虚拟节点
if (oldVnode.sel == undefined) {
oldVnode = vnode(oldVnode.tagName.toLowerCase(), {}, [], oldVnode.textContent, oldVnode)
}
//判断是不是相同标签类型
if (oldVnode.sel == newVnode.sel) {
//是的话再层层对比
patchVnode(oldVnode, newVnode)
} else {
//不是的话直接覆盖
let newVnodeElm = createElm(newVnode)
let oldVnodeElm = oldVnode.elm;
//创建节点
if (newVnodeElm) {
oldVnodeElm.parentNode.insertBefore(newVnodeElm, oldVnodeElm);
}
//清除旧节点
oldVnodeElm.parentNode.removeChild(oldVnodeElm);
}
}

8.createElm.js 创建节点函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
export default function createElm(vnode) {
let vnodeElm = document.createElement(vnode.sel);
if (vnode.children == undefined) {
vnodeElm.innerText = vnode.params
} else if (Array.isArray(vnode.children)) {
//递归创建节点
for (let child of vnode.children) {
let childDom = createElm(child)
vnodeElm.appendChild(childDom);
}
}
vnode.elm = vnodeElm;
return vnodeElm
}

9.patchVnode.js 手写节点对比函数

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
import createElm from "./createEle"
import updateChildren from "./updateChildren" //将子节点进行对比
export default function (oldVnode, newVnode) {
//判断新节点有没有子节点
if (newVnode.children === undefined) {
//没有就判断文字内容,属性,样式等
if (newVnode.params !== oldVnode.params) {
console.log(oldVnode, newVnode);
oldVnode.elm.innerText = newVnode.params
}
} else {
//有
// 新的有子节点,旧的也有子节点
if (newVnode.children && oldVnode.children) {
console.log("update");
updateChildren(oldVnode, newVnode)
}
// 新的有子节点,旧的没有子节点
if (newVnode.children && !oldVnode.children) {
//删除旧节点内容
oldVnode.elm.innerHtml = ''
//递归创建子节点
for (let v of newVnode.children) {
let childElm = createElm(v)
oldVnode.elm.appendChild(childElm);
}
}
}

}

10. 手写子节点对比函数 updateChildren.js (重点) diff 算法的精髓

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
100
101
102
import patchVnode from "./patchVnode"
import createElm from "./createEle"
function same(oldVn, newVn) {
return oldVn.data.key == newVn.data.key
}
export default function (oldVnode, newVnode) {
const oElm = oldVnode.elm//老节点的根节点
const oClm = oldVnode.children//老节点的子节点
const nClm = newVnode.children//新节点的子节点
let oldStarIndex = 0;//旧前
let oldEndIndex = oClm.length - 1//旧后
let newStarIndex = 0;//新前
let newEndIndex = nClm.length - 1//新后
//虚拟节点
let oldStarVnode = oClm[0];//旧前
let oldEndVnode = oClm[oldEndIndex]//旧后
let newStarVnode = nClm[0];//新前
let newEndVnode = nClm[newEndIndex]//新后
while (oldStarIndex <= oldEndIndex && newStarIndex <= newEndIndex) {
//判断是否为undefined
if (oldStarVnode == undefined) {
oldStarVnode = oClm[++oldStarIndex]
} else if (oldEndVnode == undefined) {
oldEndVnode = oClm[--oldEndIndex]
}
else if (same(oldStarVnode, newStarVnode)) {
console.log(1);
// 1.旧前--新前
patchVnode(oldStarVnode, newStarVnode)
// 相同:指针++继续比较下一个节点
if (newStarVnode) newStarVnode.elm = oldStarVnode.elm
oldStarVnode = oClm[++oldStarIndex]//旧前
newStarVnode = nClm[++newStarIndex]//新前
} else if (same(oldEndVnode, newEndVnode)) {
console.log(2);

// 2.旧后--新后
patchVnode(oldEndVnode, newEndVnode)
// 相同:指针++继续比较下一个节点
if (newEndVnode) newEndVnode.elm = oldEndVnode.elm
oldEndVnode = oClm[--oldEndIndex]//旧后
newEndVnode = nClm[--newEndIndex]//新后
} else if (same(oldStarVnode, newEndVnode)) {
console.log(3);
// 3.旧前--新后(旧的前一个和新的后一个)
patchVnode(oldStarVnode, newEndVnode)
//旧前指定节点移动到旧后
if (newEndVnode) newEndVnode.elm = oldStarVnode.elm
oElm.insertBefore(oldStarVnode.elm, oldEndVnode.elm.nextSibling());
oldStarVnode = oClm[++oldStarIndex]//旧前
newEndVnode = nClm[--newEndIndex]//新后
} else if (same(oldEndVnode, newStarVnode)) {
console.log(4);
// 4.旧后--新前
patchVnode(oldEndVnode, newStarVnode)
// 相同:指针++继续比较下一个节点
if (newStarVnode) newStarVnode.elm = oldEndVnode.elm
oElm.insertBefore(oldEndVnode.elm, oldStarVnode.elm.nextSibling());
oldEndVnode = oClm[--oldEndIndex]//旧后
newStarVnode = nClm[++newStarIndex]//新前
} else {
console.log(5);
//都不满足条件,查找
let keyMap = {}
for (let i = oldStarIndex; i <= oldEndIndex; i++) {
const key = oClm[i].data.key
if (key) keyMap[key] = i
}
//在老节点里面找与新节点key一样的标签
let idxInOld = keyMap[newStarVnode.data.key]
//判断是否有一样的key
if (idxInOld) {
//有
const Elm = oClm[idxInOld];
patchVnode(Elm, newStarVnode);
//处理过的设置为undefined
oClm[idxInOld] = undefined
oElm.insertBefore(Elm.elm, oldStarVnode.elm)
} else {
//没有
oElm.insertBefore(createElm(newStarVnode), oldStarVnode.elm)
}
//新数据指针+1
newStarVnode = nClm[++newStarIndex]
}
}
//结束后删除和创建元素
//1.oldStartindex>oldEndindex
//2.newstartIndex>newEndindex
if (oldStarIndex > oldEndIndex) {
const before = nClm[newEndIndex + 1] ? nClm[newEndIndex + 1].elm : null
//删除操作
for (let i = newStarIndex; i <= newEndIndex; i++) {
oElm.insertBefore(createElm(nClm[i]), before);
}
} else {
//删除操作
for (let i = oldStarIndex; i <= oldEndIndex; i++) {
oElm.removeChild(oClm[i].elm);
}
}
}

11.package.json 配置函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
{
"name": "diff",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "webpack-dev-server --open"
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"webpack": "^4.47.0",
"webpack-cli": "^3.3.12",
"webpack-dev-server": "^3.11.3"
}
}

12.TIP: 如果有报错请使用 webpack@5 4 3 顺序的包

总结:

# 1.Vue2 和 Vue3diff 算法类型区别

  1. Diff 算法实现方式的区别:Vue2 使用了双指针的对比算法,而 Vue3 使用了更便捷的静态分析法
  2. 过程:Vue2 使用了深度优先的遍历算法,会遍历整个 Dom 进行比较更新;而 Vue3 使用了递归插入法,只对有变化的节点进行更新
  3. Patch 颗粒度:Vue2 的颗粒度是组件级别的,遍历整个 DOM 树进行比较;而 Vue3 的颗粒度是组件内节点级别地只会对比发生改变地节点;
  4. Key 值地处理:Vue2 中必须加入 Key 值才能进行比较,而 Vue3 使用了 Fragments 节点和片段节点,不需要再手动地指定 Key 属性,就可以进行比较更新

# diff 算法核心:再根节点相同,且都有子节点的情况

1. 旧前 -- 新前

相同:指针 ++ 继续比较下一个节点

不同:下一步

2. 旧后 -- 新后

相同:指针 -- 继续比较上一个节点

不同:下一步

3. 旧前 -- 新后

同:旧 ++,新 --

4. 旧后 -- 新前

同:旧 --,新 ++

5. 都不符合》

新中找旧,等位替换,查找到后赋值为 undefined

更新于 阅读次数

请我喝[茶]~( ̄▽ ̄)~*

Xiao Yang Guo 微信支付

微信支付

Xiao Yang Guo 支付宝

支付宝

Xiao Yang Guo 贝宝

贝宝