Vdom

VNode

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
export default class VNode {
constructor(
tag?: string,
data?: VNodeData,
children?: ?Array<VNode>,
text?: string,
elm?: Node,
context?: Component,
componentOptions?: VNodeComponentOptions,
asyncFactory?: Function
) {
this.tag = tag; /*当前节点的标签名*/
this.data =
data; /*当前节点对应的对象,包含了具体的一些数据信息,是一个VNodeData类型,可以参考VNodeData类型中的数据信息*/
this.children = children; /*当前节点的子节点,是一个数组*/
this.text = text; /*当前节点的文本*/
this.elm = elm; /*当前虚拟节点对应的真实dom节点*/
this.ns = undefined; /*当前节点的名字空间*/
this.context = context; /*当前组件节点对应的Vue实例*/
this.fnContext = undefined; /*函数式组件对应的Vue实例*/
this.fnOptions = undefined;
this.fnScopeId = undefined;
this.key = data && data.key; /*节点的key属性,被当作节点的标志,用以优化*/
this.componentOptions = componentOptions; /*组件的option选项*/
this.componentInstance = undefined; /*当前节点对应的组件的实例*/
this.parent = undefined; /*当前节点的父节点*/
this.raw =
false; /*简而言之就是是否为原生HTML或只是普通文本,innerHTML的时候为true,textContent的时候为false*/
this.isStatic = false; /*静态节点标志*/
this.isRootInsert = true; /*是否作为跟节点插入*/
this.isComment = false; /*是否为注释节点*/
this.isCloned = false; /*是否为克隆节点*/
this.isOnce = false; /*是否有v-once指令*/
this.asyncFactory = asyncFactory;
this.asyncMeta = undefined;
this.isAsyncPlaceholder = false;
}

get child(): Component | void {
return this.componentInstance;
}
}

节点类型

注释节点

createEmptyVNode + isComment

1
2
3
4
5
6
export const createEmptyVNode = (text: string = "") => {
const node = new VNode();
node.text = text;
node.isComment = true;
return node;
};

文本节点

createTextVNode

1
2
3
export function createTextVNode(val: string | number) {
return new VNode(undefined, undefined, undefined, String(val));
}

元素节点

tag attributes

组件节点

组件节点除了有元素节点具有的属性之外,它还有两个特有的属性

  • componentOptions: 组件的 option 选项,如组件的 props 等
  • componentInstance: 当前组件节点对应的 Vue 实例

函数式组件节点

函数式组件节点相较于组件节点,它又有两个特有的属性

  • fnContext: 函数式组件对应的 Vue 实例
  • fnOptions: 组件的 option 选项

克隆节点

cloneVNode + isCloned

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
export function cloneVNode(vnode: VNode): VNode {
const cloned = new VNode(
vnode.tag,
vnode.data,
vnode.children,
vnode.text,
vnode.elm,
vnode.context,
vnode.componentOptions,
vnode.asyncFactory
);
cloned.ns = vnode.ns;
cloned.isStatic = vnode.isStatic;
cloned.key = vnode.key;
cloned.isComment = vnode.isComment;
cloned.fnContext = vnode.fnContext;
cloned.fnOptions = vnode.fnOptions;
cloned.fnScopeId = vnode.fnScopeId;
cloned.asyncMeta = vnode.asyncMeta;
cloned.isCloned = true;
return cloned;
}

Dom-Diff

patch 过程,主要有三个过程,创建节点,删除节点,更新节点

创建节点

新的 VNode 中有而旧的 oldVNode 中没有,就在旧的 oldVNode 中创建。判断三类节点

  • 元素节点: 判断该 VNode 节点是否有 tag 标签 + createElement
  • 注释节点: isComment + createComment
  • 文本节点: createTextNode
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function createElm(vnode, parentElm, refElm) {
const data = vnode.data;
const children = vnode.children;
const tag = vnode.tag;
if (isDef(tag)) {
vnode.elm = nodeOps.createElement(tag, vnode); // 创建元素节点
createChildren(vnode, children, insertedVnodeQueue); // 创建元素节点的子节点
insert(parentElm, vnode.elm, refElm); // 插入到DOM中
} else if (isTrue(vnode.isComment)) {
vnode.elm = nodeOps.createComment(vnode.text); // 创建注释节点
insert(parentElm, vnode.elm, refElm); // 插入到DOM中
} else {
vnode.elm = nodeOps.createTextNode(vnode.text); // 创建文本节点
insert(parentElm, vnode.elm, refElm); // 插入到DOM中
}
}

删除节点

新的 VNode 中没有而旧的 oldVNode 中有,就从旧的 oldVNode 中删除

1
2
3
4
5
6
function removeNode(el) {
const parent = nodeOps.parentNode(el); // 获取父节点
if (isDef(parent)) {
nodeOps.removeChild(parent, el); // 调用父节点的removeChild方法
}
}

更新节点

新的 VNode 和旧的 oldVNode 中都有,就以新的 VNode 为准,更新旧的 oldVNode。分为几种情况

VNode / oldVNode 均为静态节点

isStatic,跳过,无需处理

VNode 为文本节点

VNode 为元素节点

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
function patchVnode(oldVnode, vnode, insertedVnodeQueue, removeOnly) {
// vnode与oldVnode是否完全一样?若是,退出程序
if (oldVnode === vnode) {
return;
}
const elm = (vnode.elm = oldVnode.elm);

// vnode与oldVnode是否都是静态节点?若是,退出程序
if (
isTrue(vnode.isStatic) &&
isTrue(oldVnode.isStatic) &&
vnode.key === oldVnode.key &&
(isTrue(vnode.isCloned) || isTrue(vnode.isOnce))
) {
return;
}

const oldCh = oldVnode.children;
const ch = vnode.children;
// vnode有text属性?若没有:
if (isUndef(vnode.text)) {
// vnode的子节点与oldVnode的子节点是否都存在?
if (isDef(oldCh) && isDef(ch)) {
// 若都存在,判断子节点是否相同,不同则更新子节点
if (oldCh !== ch)
updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly);
}
// 若只有vnode的子节点存在
else if (isDef(ch)) {
/**
* 判断oldVnode是否有文本?
* 若没有,则把vnode的子节点添加到真实DOM中
* 若有,则清空Dom中的文本,再把vnode的子节点添加到真实DOM中
*/
if (isDef(oldVnode.text)) nodeOps.setTextContent(elm, "");
addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue);
}
// 若只有oldnode的子节点存在
else if (isDef(oldCh)) {
// 清空DOM中的子节点
removeVnodes(elm, oldCh, 0, oldCh.length - 1);
}
// 若vnode和oldnode都没有子节点,但是oldnode中有文本
else if (isDef(oldVnode.text)) {
// 清空oldnode文本
nodeOps.setTextContent(elm, "");
}
// 上面两个判断一句话概括就是,如果vnode中既没有text,也没有子节点,那么对应的oldnode中有什么就清空什么
}
// 若有,vnode的text属性与oldVnode的text属性是否相同?
else if (oldVnode.text !== vnode.text) {
// 若不相同: 则用vnode的text替换真实DOM的文本
nodeOps.setTextContent(elm, vnode.text);
}
}

更新子节点

双层循环遍历 newChildren 和 oldChildren: 每循环外层 newChildren 数组里的一个子节点,就去内层 oldChildren 数组里找看有没有与之相同的子节点。分为几种情况

创建子节点

newChildren 里有,而 oldChildren 里没有: 创建节点,创建好之后再把它插入到 DOM 中合适的位置

合适的位置是所有未处理节点之前,而并非所有已处理节点之后

下图节点顺序插入出错

删除子节点

newChildren 里没有,而 oldChildren 里有

更新子节点

newChildren 里有,且 oldChildren 里有,且位置相同

移动子节点

newChildren 里有,且 oldChildren 里有,但位置不同: 以 newChildren 里子节点的位置为基准,调整 oldChildren 里该节点的位置,使之与在 newChildren 里的位置相同

  • 以 newChildren 里子节点的位置为基准,调整 oldChildren 里该节点的位置

所有未处理节点之前就是要移动的目的位置

diff 对比过程

对比顺序: 4 指针双向遍历

  • newStartIdx: newChildren 数组里开始位置的下标
  • newEndIdx: newChildren 数组里结束位置的下标
  • oldStartIdx: oldChildren 数组里开始位置的下标
  • oldEndIdx: oldChildren 数组里结束位置的下标
  • 在循环的时候,每处理一个节点,newStartIdx 和 oldStartIdx 往后 +1 (只会加),newEndIdx 和 oldEndIdx 往前 -1 (只会减),当开始位置大于结束位置时,表示所有节点都已经遍历过了

优化前策略

  1. 如果 oldStartVnode 不存在,则直接跳过,将 oldStartIdx 加 1,比对下一个
  2. 如果 oldEndVnode 不存在,则直接跳过,将 oldEndIdx 减 1,比对前一个
  3. 如果新前与旧前节点相同,就把两个节点进行 patch 更新,同时 oldStartIdx 和 newStartIdx 都加 1,后移一个位置
  4. 如果新后与旧后节点相同,就把两个节点进行 patch 更新,同时 oldEndIdx 和 newEndIdx 都减 1,前移一个位置
  5. 如果新后与旧前节点相同,先把两个节点进行 patch 更新,然后把旧前节点移动到 oldChilren 中所有未处理节点之后,最后把 oldStartIdx 加 1,后移一个位置,newEndIdx 减 1,前移一个位置
  6. 如果新前与旧后节点相同,先把两个节点进行 patch 更新,然后把旧后节点移动到 oldChilren 中所有未处理节点之前,最后把 newStartIdx 加 1,后移一个位置,oldEndIdx 减 1,前移一个位置
  7. 如果不属于以上四种情况,就进行常规的循环比对 patch
  8. 如果在循环中,oldStartIdx 大于 oldEndIdx 了,表示 oldChildren 比 newChildren 先循环完毕,那么 newChildren 里面剩余的节点都是需要新增的节点,把[newStartIdx, newEndIdx]之间的所有节点都插入到 DOM 中
  9. 如果在循环中,newStartIdx 大于 newEndIdx 了,表示 newChildren 比 oldChildren 先循环完毕,那么 oldChildren 里面剩余的节点都是需要删除的节点,把[oldStartIdx, oldEndIdx]之间的所有节点都删除

优化后策略

  1. 新前比旧前: 相同则进入更新节点流程,不同则进入下一步
  2. 新后比旧后: 相同则进入更新节点流程,不同则进入下一步
  3. 新后比旧前: 相同则进入更新节点流程,更新完后再将旧前移动到 oldVNode 数组中所有未处理节点之后,不同则进入下一步
  4. 新前比旧后: 相同则进入更新节点流程,更新完后再将旧后移动到 oldVNode 数组中所有未处理节点之前,不同则通过之前的循环方式查找
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
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
// 循环更新子节点
function updateChildren(
parentElm,
oldCh,
newCh,
insertedVnodeQueue,
removeOnly
) {
let oldStartIdx = 0; // oldChildren开始索引
let oldEndIdx = oldCh.length - 1; // oldChildren结束索引
let oldStartVnode = oldCh[0]; // oldChildren中所有未处理节点中的第一个
let oldEndVnode = oldCh[oldEndIdx]; // oldChildren中所有未处理节点中的最后一个

let newStartIdx = 0; // newChildren开始索引
let newEndIdx = newCh.length - 1; // newChildren结束索引
let newStartVnode = newCh[0]; // newChildren中所有未处理节点中的第一个
let newEndVnode = newCh[newEndIdx]; // newChildren中所有未处理节点中的最后一个

let oldKeyToIdx, idxInOld, vnodeToMove, refElm;

// removeOnly is a special flag used only by <transition-group>
// to ensure removed elements stay in correct relative positions
// during leaving transitions
const canMove = !removeOnly;

if (process.env.NODE_ENV !== "production") {
checkDuplicateKeys(newCh);
}

// 以"新前"、"新后"、"旧前"、"旧后"的方式开始比对节点
while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
if (isUndef(oldStartVnode)) {
oldStartVnode = oldCh[++oldStartIdx]; // 如果oldStartVnode不存在,则直接跳过,比对下一个
} else if (isUndef(oldEndVnode)) {
oldEndVnode = oldCh[--oldEndIdx];
} else if (sameVnode(oldStartVnode, newStartVnode)) {
// 如果新前与旧前节点相同,就把两个节点进行patch更新
patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue);
oldStartVnode = oldCh[++oldStartIdx];
newStartVnode = newCh[++newStartIdx];
} else if (sameVnode(oldEndVnode, newEndVnode)) {
// 如果新后与旧后节点相同,就把两个节点进行patch更新
patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue);
oldEndVnode = oldCh[--oldEndIdx];
newEndVnode = newCh[--newEndIdx];
} else if (sameVnode(oldStartVnode, newEndVnode)) {
// Vnode moved right
// 如果新后与旧前节点相同,先把两个节点进行patch更新,然后把旧前节点移动到oldChilren中所有未处理节点之后
patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue);
canMove &&
nodeOps.insertBefore(
parentElm,
oldStartVnode.elm,
nodeOps.nextSibling(oldEndVnode.elm)
);
oldStartVnode = oldCh[++oldStartIdx];
newEndVnode = newCh[--newEndIdx];
} else if (sameVnode(oldEndVnode, newStartVnode)) {
// Vnode moved left
// 如果新前与旧后节点相同,先把两个节点进行patch更新,然后把旧后节点移动到oldChilren中所有未处理节点之前
patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue);
canMove &&
nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm);
oldEndVnode = oldCh[--oldEndIdx];
newStartVnode = newCh[++newStartIdx];
} else {
// 如果不属于以上四种情况,就进行常规的循环比对patch
if (isUndef(oldKeyToIdx))
oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx);
idxInOld = isDef(newStartVnode.key)
? oldKeyToIdx[newStartVnode.key]
: findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx);
// 如果在oldChildren里找不到当前循环的newChildren里的子节点
if (isUndef(idxInOld)) {
// New element
// 新增节点并插入到合适位置
createElm(
newStartVnode,
insertedVnodeQueue,
parentElm,
oldStartVnode.elm,
false,
newCh,
newStartIdx
);
} else {
// 如果在oldChildren里找到了当前循环的newChildren里的子节点
vnodeToMove = oldCh[idxInOld];
// 如果两个节点相同
if (sameVnode(vnodeToMove, newStartVnode)) {
// 调用patchVnode更新节点
patchVnode(vnodeToMove, newStartVnode, insertedVnodeQueue);
oldCh[idxInOld] = undefined;
// canmove表示是否需要移动节点,如果为true表示需要移动,则移动节点,如果为false则不用移动
canMove &&
nodeOps.insertBefore(parentElm, vnodeToMove.elm, oldStartVnode.elm);
} else {
// same key but different element. treat as new element
createElm(
newStartVnode,
insertedVnodeQueue,
parentElm,
oldStartVnode.elm,
false,
newCh,
newStartIdx
);
}
}
newStartVnode = newCh[++newStartIdx];
}
}
if (oldStartIdx > oldEndIdx) {
/**
* 如果oldChildren比newChildren先循环完毕,
* 那么newChildren里面剩余的节点都是需要新增的节点,
* 把[newStartIdx, newEndIdx]之间的所有节点都插入到DOM中
*/
refElm = isUndef(newCh[newEndIdx + 1]) ? null : newCh[newEndIdx + 1].elm;
addVnodes(
parentElm,
refElm,
newCh,
newStartIdx,
newEndIdx,
insertedVnodeQueue
);
} else if (newStartIdx > newEndIdx) {
/**
* 如果newChildren比oldChildren先循环完毕,
* 那么oldChildren里面剩余的节点都是需要删除的节点,
* 把[oldStartIdx, oldEndIdx]之间的所有节点都删除
*/
removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx);
}
}