深入浅出 VUE 的 DIFF 算法实现

前言

vue版本:2.6.10

每次更新视图前都会根据视图模板生成vnode(虚拟的节点树),vnode类似dom树,但更简陋,每个vnode都与页面的上的元素html元素一一对应!为了更好的性能,因此要复用元素。那么就要知道怎么复用!就要对比newVnode(当前生成的vnode)和oldVnode(上次生成的vnode),对比完之后才知道那些是要删除,那些是需要重新创建,那些需要移动、移动到哪里!?
而diff算法则是对比的一种比较好的方式,更好的更快地对比,谁被谁复用!

newVnode和oldVnode的比对仅限于同层级之间对比,兄弟之间相互比较,如下图。不会出现跨层级的对比。

vue中的diff算法实现

diff算法是什么

回到顶部

diff算法不是一种对比的方法,而是一种寻找与当前节点匹配可复用节点的方法;寻找oldVnode.children中那个成员与newVnode.children中那个成员相同。

这种寻找的方法如图可见一斑,主要的方法有5种,辅助的有2种(未画在图上),一共7种。每种寻找方式相互独立!通过循环遍历children,逐一判断,直到循环结束。下面结合代码分别说明其中寻找方式。

ps:无需过于在意图中所表达的逻辑,图只是用于辅助说明下面的源码

vue中的diff算法实现
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function updateChildren (
parentElm, // {Element},父节点的真实html元素
oldCh, // {Vnode[]},oldVnode.children
newCh, // {Vnode[]},newVnode.children
insertedVnodeQueue, // {Vnode[]},插入的节点队列(此时可忽略)
removeOnly // {Boolean},是否只可以删除
) {
let oldStartIdx = 0
let newStartIdx = 0
let oldEndIdx = oldCh.length - 1
let oldStartVnode = oldCh[0]
let oldEndVnode = oldCh[oldEndIdx]
let newEndIdx = newCh.length - 1
let newStartVnode = newCh[0]
let newEndVnode = newCh[newEndIdx]
let oldKeyToIdx, idxInOld, vnodeToMove, refElm

// ...
}

建立四个指针oldStartVnodeoldEndVnodenewStartVnodenewEndVnode,由updateChildren中的定义可以知道:开始时,他们分别指向oldVnode.children的头部、oldVnode.children的尾部、newVnode.children的头部、newVnode.children的尾部。然后,这四个指针的指向也不是固定的,在循环遍历的过程中,他们的指向也会变动,他们指向会因为以下索引的变动而变动,oldStartIdxoldEndIdxnewStartIdxnewEndIdx

1.新头与旧头垂直对比

回到顶部

vue中的diff算法实现

新旧头部vnode进行对比,判断是否匹配,以复用。sameVnode的功能与实现逻辑参考附录:sameVnode的功能与实现逻辑,值得一提的是:a.是input元素,更新前后type不一致;b.变动的是key属性;c.元素更新前后将所有属性删除,或从无到有;只要不是以上三种情况之一,不论怎么增删、修改元素上的属性,都不会影响是否匹配的结果!

1.判断新旧头部是匹配的,那么就调用patchVnode,给newStartVnode打补丁!

patchVnode函数的主要功能:

a. 复用elm,将oldVnode.elm赋值到newVnode.elm;
b. 更新elm上的属性变动;
c. 更新newVnode.children,增删或复用,这里的复用就是通过调用updateChildren来实现,没错递归了!

patchVnode函数的主要功能参考:附录:patchVnode函数的关键实现

2.分别右移oldStartVnodenewStartVnode

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function updateChildren (/* */) {
// ...
while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
// ...

/* 1 */
else if (sameVnode(oldStartVnode, newStartVnode)) {
patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
oldStartVnode = oldCh[++oldStartIdx]
newStartVnode = newCh[++newStartIdx]
}
// ...
}
// ...
}

2.新尾与旧尾垂直对比

回到顶部

vue中的diff算法实现

新旧尾部的对比情况和[1新头与旧头垂直对比]类似,再次再累累述,以下实现的逻辑:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function updateChildren (/* */) {
// ...
while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
// ...

/* 2 */
else if (sameVnode(oldEndVnode, newEndVnode)) {
patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue, newCh, newEndIdx)
oldEndVnode = oldCh[--oldEndIdx]
newEndVnode = newCh[--newEndIdx]
}
// ...
}
// ...
}

3.新尾与旧头交叉对比

回到顶部

vue中的diff算法实现

当前情况与[1新头与旧头垂直对比]略有不同!看源码中,多出了下面这句:

1
canMove && nodeOps.insertBefore(parentElm, oldStartVnode.elm, nodeOps.nextSibling(oldEndVnode.elm))

这句代码实现就是图片中移动elm的功能!为什么要移动elm?因为newEndVnode复用了oldStartVnode.elm,复用这一步已经由patchVnode函数实现,然后还需要让elm列的顺序与newVnode的顺序保持一致,所以需要将oldStartVnode.elm移动到正确的位置!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
function updateChildren (/* */) {
// ...
while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
// ...

/* 3 */
else if (sameVnode(oldStartVnode, newEndVnode)) { // Vnode moved right
patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue, newCh, newEndIdx)
/**
* Node.insertBefore() 方法在参考节点之前插入一个拥有指定父节点的子节点。
* 如果给定的子节点是对文档中现有节点的引用,insertBefore() 会将其从当前位置移动到新位置
*/
// canMove && 在parentElm的nodeOps.nextSibling(oldEndVnode.elm)前面插入oldStartVnode.elm
// 换言之,在 oldEndVnode.elm 前面插入 oldStartVnode.elm
// 旧children的 头部真实元素 移动到 尾部真实元素的后面
canMove && nodeOps.insertBefore(parentElm, oldStartVnode.elm, nodeOps.nextSibling(oldEndVnode.elm))
oldStartVnode = oldCh[++oldStartIdx]
newEndVnode = newCh[--newEndIdx]
}
// ...
}
// ...
}

4.新头与旧尾交叉对比

回到顶部

vue中的diff算法实现

当前情况与[新尾与旧头交叉对比]类似,不做赘述!配合图片和源码食用口味更佳~

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function updateChildren (/* */) {
// ...
while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
// ...

/* 4 */
else if (sameVnode(oldEndVnode, newStartVnode)) { // Vnode moved left
patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
// 将旧children的尾部真实元素移动到头部真实元素的后面
canMove && nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm)
oldEndVnode = oldCh[--oldEndIdx]
newStartVnode = newCh[++newStartIdx]
}
// ...
}
// ...
}

5.当前新vnode与旧头尾之间的vnode对比

回到顶部

在本次循环中,前4种控制流都没有进入,就说明一头一尾、两次交叉对比都没有找到可复用的节点!但这并非代表旧children中无可复用,因为头与尾之间的元素还没有比对过,第5种方式即是如此!这第5种方式在有定义key(v-for指令中的key)或没有的情况下又是不同的表现!

注意:在此情况下,是用新头去旧children的头尾之间寻找可复用元素

5-1.构建oldCildren映射表(key => idx)

从oldChildren构建一个映射表(key => idx),这样就可以通过key,结合这个映射表快速找到匹配的可复用的元素。时间复杂度就是O(1)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function updateChildren (/* */) {
// ...
while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
// ...

/* 5:当前新vnode与旧头尾之间的vnode对比 */
else {
/* 5-1 */
// 只会执行一次,第一次定义映射表
if (isUndef(oldKeyToIdx)) {
// 创建对象映射表,children.key => children.i, i ∈ [oldStartIdx, oldEndIdx]
oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)
}
// ...
newStartVnode = newCh[++newStartIdx]
}
// ...
}
// ...
}

createKeyToOldIdx的实现:

1
2
3
4
5
6
7
8
9
function createKeyToOldIdx (children, beginIdx, endIdx) {
let i, key
const map = {}
for (i = beginIdx; i <= endIdx; ++i) {
key = children[i].key
if (isDef(key)) map[key] = i
}
return map
}

5-2.根据5-1的映射表找到可复用vnode的索引

列表渲染中不一定会定义key,如果没有定义那么5-1的映射表就没有用了。那么就需要遍历旧children节点寻找与新头匹配的元素(详见下面代码的findIdxInOld方法)!那么时间复杂度就上来了,不再是使用映射表时的O(1),而是O(n)。由此也可以知道使用key的性能优化优越之所在!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
function updateChildren (/* */) {
// ...
while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
// ...

/* 5:当前新vnode与旧头尾之间的vnode对比 */
else {
// ...

/* 5.2 */
// 定义key,直接在名射表找,时间复杂度: O(1)
// 没有定义key,用新vnode与旧vnode数组比对,时间复杂度:O(n)
// const isDef = (v) => v !== undefined && v !== null
idxInOld = isDef(newStartVnode.key)
? oldKeyToIdx[newStartVnode.key]
// 返回oldCh中与newStartVnode相同( sameVnode(newStartVnode, oldCh[itIdx]) )节点(即isDef(oldCh[itIdx].key) 同样是false)的index
: findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx)
// ...
newStartVnode = newCh[++newStartIdx]
}
// ...
}
// ...
}

findIdxInOld:时间复杂度O(n)

1
2
3
4
5
6
function findIdxInOld (node, oldCh, start, end) {
for (let i = start; i < end; i++) {
const c = oldCh[i]
if (isDef(c) && sameVnode(node, c)) return i
}
}

5-3.无可复用旧元素

在旧children可能会找到也可能找不到可复用的元素,没有找到是什么情况?如图:

vue中的diff算法实现:在头尾见找可复用元素

假如现在newStartVnode指向的是key = 1.5的vnode,那么很明显旧children中就没有可以复用的vnode,那么需要做的就是:a.创建一个与newStartVnode对应的newElm(新的真实html元素);b.然后将newElm插入到旧children中key=02的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
function updateChildren (/* */) {
// ...
while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
// ...

/* 5:当前新vnode与旧头尾之间的vnode对比 */
else {
// ...

/* 5.3 */
// 在旧虚拟节点中不存在新节点,无法复用旧元素
/**
* [ 1 ] [ 2 ] [ 3 ] [ 4 ] [ 5 ]
* [ 1 ] [ 2 ] [2.5] [ 3 ] [ 4 ] [ 5 ]
* [2.5]就是插入的,且就children中没有与之“相同”的vnode
* 目前 newStartIdx = oldStartIdx = 2
* 那么现在需要做的是:a.创建一个与[2.5]对应的真实元素;b.将元素插入到 [ 2 ] 后面 [ 3 ]前面
* nodeOps.insertBefore(parentElm, newElm, oldStartVnode.elm)
*/
if (isUndef(idxInOld)) { // New element
// 创建 newStartVnode 对应的elm,将elm插入到parentElm的子元素oldStartVnode.elm的前面(如果oldStartVnode.elm不存在即插入到parentElm的最后)
createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx)
}
// ...
newStartVnode = newCh[++newStartIdx]
}
// ...
}
// ...
}

5-4.复用旧元素

5-3和5-4是互斥的,进入5-4控制流就表示5-2中返回的idxInOld不为空,旧children中存在这匹配的vnode。虽然存在可用的vnode,但如果key并不可信呢?比如v-for="(item, index) in items"中的索引被用作key!!!因此有了下面的5-4-1和5-4-2。

5-4-1.确实可复用

使用sameVnode方法二次确认vnodeToMove(在旧children中找到的vnode)时可用的!接下就是类似的操作。但比较明显的不同是:其他都是递增或递减新旧索引,但在5-4-1中则是递增newStartIdx,然后旧vnode置为null(oldCh[idxInOld] = undefined),这是设计的巧妙之处,当前还没有感受到,再看下-1.跳过左边已经复用的vnode0.跳过右边已经复用的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
function updateChildren (/* */) {
// ...
while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
// ...

/* 5:当前新vnode与旧头尾之间的vnode对比 */
else {
// ...

/* 5-4 */
// 在旧虚拟节点中存在新节点
else {
/* 5-4-1 */
vnodeToMove = oldCh[idxInOld]
// 保证节点的key和虚拟节点都相同( oldKeyToIdx[newStartVnode.key] 获取的idxInOld,指向
// 的虚拟节点可能与newStartVnode节点不一样(!sameVnode) )
if (sameVnode(vnodeToMove, newStartVnode)) {
patchVnode(vnodeToMove, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
oldCh[idxInOld] = undefined
canMove && nodeOps.insertBefore(parentElm, vnodeToMove.elm, oldStartVnode.elm)
}
}
// ...
newStartVnode = newCh[++newStartIdx]
}
// ...
}
// ...
}

5-4-2.虚假的可复用

5-4-1与5-4-2是互斥的,既然没有元素可以复用到newStartVnode中,那么只能像5-3中那样创建与newStartVnode对应的html元素!!!

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
function updateChildren (/* */) {
// ...
while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
// ...

/* 5:当前新vnode与旧头尾之间的vnode对比 */
else {
// ...

/* 5-4 */
// 在旧虚拟节点中存在新节点
else {
/* 5-4-2 */
else {
// same key but different element. treat as new element
// key相同但虚拟节点不同,newStartVnode当做新元素创建
createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx)

}
}
// ...
newStartVnode = newCh[++newStartIdx]
}
// ...
}
// ...
}

-1.跳过左边已经复用的vnode

回到顶部

我们知道oldStartVnode这个指针是不断地右移,从下面的代码中的isUndef(oldStartVnode)知道,一旦碰到未定的vnode就会右移一个单位,继续循环比对后面的vnode。为什么会有未定义的vnode?正常来说应该存在,因为vnode都是与页面上的html元素一一对应的!在5-4-1.确实可复用中,vue确实地将旧children中存在可复用elm的vnode手动置为了undefined:oldCh[idxInOld] = undefined!为什么置空不直接用delete操作符删除?!删了就换了idx顺序!!

1
2
3
4
5
6
7
8
9
10
11
12
function updateChildren (/* */) {
// ...
while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
/* -1 */
if (isUndef(oldStartVnode)) {
oldStartVnode = oldCh[++oldStartIdx] // Vnode has been moved left

}
// ...
}
// ...
}

0.跳过右边已经复用的vnode

回到顶部

参考-1.跳过左边已经复用的vnode

1
2
3
4
5
6
7
8
9
10
11
12
13
function updateChildren (/* */) {
// ...
while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
// ...

/* 0 */
else if (isUndef(oldEndVnode)) {
oldEndVnode = oldCh[--oldEndIdx]
}
// ...
}
// ...
}

while中的控制流顺序

回到顶部

上面为了突出重点去讲,没有按while中控制流的顺序书写,以下是while块总各控制流的顺序:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
/* -1:跳过左边已经复用的vnode */
if (isUndef(oldStartVnode)) { /* */ }
/* 0:跳过右边已经复用的vnode */
else if (isUndef(oldEndVnode)) { /* */ }
/* 1:新头与旧头垂直对比 */
else if (sameVnode(oldStartVnode, newStartVnode)) { /* */ }
/* 2:新尾与旧尾垂直对比 */
else if (sameVnode(oldEndVnode, newEndVnode)) { /* */ }
/* 3:新尾与旧头交叉对比 */
else if (sameVnode(oldStartVnode, newEndVnode)) { /* */ }
/* 4:新头与旧尾交叉对比 */
else if (sameVnode(oldEndVnode, newStartVnode)) { /* */ }
/* 5:当前新vnode与旧头尾之间的vnode对比 */
else { /* */ }
}

while之外

回到顶部

留意while的循环条件:oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx,只要oldStartIdx大于oldEndIdxnewStartIdx大于newEndIdx就会结束循环!换言之,只要遍历完新旧children任意一个就会结束循环!

a. 先遍历完旧children就说明新children新增了vnode,那么就要创建与这些vnodes对应的elm;
b. 先遍历完新children就说明新children删除了一些vnode,那么就要删除多出的vnodes。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
if (oldStartIdx > oldEndIdx) {
// 会用调用node.insertBefore插入新元素,现在就是找引用元素,在refElm前面插入新元素
refElm = isUndef(newCh[newEndIdx + 1])
/**
* 新的children没有新增元素(newStartIdx > newEndIdx)
* 或 后面新增了vnode(newStartIdx <= newEndIdx)
* */
? null
/**
* newStartIdx <= newEndIdx
* 新的children新增了元素,但不是在后面!
* 可能是中间!
* 也可能是在前面
* */
: newCh[newEndIdx + 1].elm

// 循环调用 createElm
addVnodes(parentElm, refElm, newCh, newStartIdx, newEndIdx, insertedVnodeQueue)
} else if (newStartIdx > newEndIdx) {
removeVnodes(oldCh, oldStartIdx, oldEndIdx)
}

因为在循环遍历children的时候,startIdx(newStartIdx或oldStartIdx)和endIdx分别会向左和右移动。下面是四个索引移动的情况:

新children新增了vnode

根据newStartIdx和newEndIdx的移动情况

1.newStartIdx一直右移,由于新增的vnode都在后面,可以复用的vnode都在前面了,newEndIdx会保持不变,直到遍历完旧children:
vue中的diff算法实现
&nbsp;

2.newStartIdx右移,newEndIdx左移,直到遍历完旧children:

vue中的diff算法实现

3.新增的vnode都在前面了,由于是新的节点所以存在“newStartIdx右移”的情况,newStartIdx就保持不变了,而可复用的vnode在右边,随着一次次循环,newEndIdx则会左移:

vue中的diff算法实现

新children删除了vnode的情况就不赘述,情况可以从上面的解析类推!

新旧vnode与真实元素elm的关系

回到顶部

vnode是和elm一一对应的,vnode的顺序和elm保持这一致,vnode上的属性也是与对应的elm的属性对应。所以,在patch(给oldVnode打补丁)前,可以认为oldVnode树与页面上elm树是对应的!

1.oldVnode.children中vnode的顺序和oldVnode.elm.children(oldVnode对应的elm的子元素列表)的顺序是保持一致的、elm上的属性也是保持一致;

2.diff算法通过对比oldVnode.children与newVnode.children的vnode,找到可以复用的elm,并改变elm的位置,使之与newVnode.children的顺序保持一致!

diff的特点

  • 先垂直,再交叉,最后中间找,diff在旧vnode.children找可复用vnode,所用比对方式的优先级!

&nbsp;

  • 只与同级vnode中寻找复用的elm,由上面的分析可以知道,只会在同级的children中寻找可以复用的vnode。但现实是可以复用的元素可以存在于dom树任意的地方,明显这样是可能回错过实际存在的复用元素,而重新创建元素!这里就是vue或diff的权衡的地方,是不计代价全局去找最优解,还是如当前这般在同级节点中寻找!?

&nbsp;

  • 定义key属性可以大幅度减少操作数,在5.当前新vnode与旧头尾之间的vnode对比中,在定义了key的情况下,会创建一个映射表oldKeyToIdx,通过映射表可以快速找到可复用vnode,而没有定义的话,就需要遍历oldVnode.children,逐一使用sameVnode比对!

实用主义

1.新头与旧头垂直对比2.新尾与旧尾垂直对比3.新尾与旧头交叉对比4.新头与旧尾交叉对比,以上四种不论是否定义元素属性key

  1. 定义了,可以快速判断出不相同(但不完全可靠)

使用遍历索引作为key,

它也可以用于强制替换元素/组件而不是重复使用它。当你遇到如下场景时它可能会很有用:

完整地触发组件的生命周期钩子
触发过渡

diff算法默认使用“就地复用”的策略,是一个首尾交叉对比的过程。
用index作为key和不加key是一样的,都采用“就地复用”的策略
“就地复用”的策略,只适用于不依赖子组件状态或临时 DOM 状态 (例如:表单输入值) 的列表渲染输出。
将与元素唯一对应的值作为key,可以最大化利用dom节点,提升性能

附录

回到顶部

sameVnode的功能与实现逻辑

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
function sameVnode (a, b) {
return (
a.key === b.key && (
(
// 标签相同
a.tag === b.tag &&

// 都是注释元素, 或都不是
a.isComment === b.isComment &&

// idDef = (v) => v !== undefined && v !== null
// 都定义了,或都没有定义
isDef(a.data) === isDef(b.data) &&

// a = { data: { atttrs: { type: 'xxx' } } }
// 1. 两节点的type相同,
// i. type存在, 且相同;
// ii. 两个type都没有定义,都是undefined;a、b都算是通过
// 2. a、b节点type都是'text,number,password,search,email,tel,url'中之一
// 换言之 a.type = text, b.type = password,也可以说两个input节点相同
// 3. a不是input标签
sameInputType(a, b)
) || (
isTrue(a.isAsyncPlaceholder) &&
a.asyncFactory === b.asyncFactory &&
isUndef(b.asyncFactory.error)
)
)
)
}

/**
* makeMap是个工厂函数,生成 isTextInputType = (key) => {
* const map = { text: true, ..., url: true };
* return map[key];
* }
*
* 类似于 (val) => [text,number,password,search,email,tel,url].include(val);
* */
const isTextInputType = makeMap('text,number,password,search,email,tel,url')
function sameInputType (a, b) {
if (a.tag !== 'input') return true
let i
const typeA = isDef(i = a.data) && isDef(i = i.attrs) && i.type
const typeB = isDef(i = b.data) && isDef(i = i.attrs) && i.type
return typeA === typeB || isTextInputType(typeA) && isTextInputType(typeB)
}

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 (/* */) {
// ...

// a. 复用elm,将oldVnode.elm赋值到newVnode.elm;
const elm = vnode.elm = oldVnode.elm

const oldCh = oldVnode.children
const ch = vnode.children

// b. 更新elm上的属性变动;
if (isDef(data) && isPatchable(vnode)) {
for (i = 0; i < cbs.update.length; ++i) {
cbs.update[i](oldVnode, vnode);
}
if (isDef(i = data.hook) && isDef(i = i.update)) i(oldVnode, vnode)
}

// 没有文本,即是还有子节点等情况
if (isUndef(vnode.text)) {
// 新旧vnode都有children
if (isDef(oldCh) && isDef(ch)) {
// c. 更新newVnode.children,增删或复用,这里的复用就是通过调用`updateChildren`来实现,没错递归了!
if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly)
}
// 省略其他的更新children的操作:增、删等
}
// ...
}

nodeOps.insertBefore实现

path: src/platforms/web/runtime/node-ops.js

1
2
3
export function insertBefore (parentNode: Node, newNode: Node, referenceNode: Node) {
parentNode.insertBefore(newNode, referenceNode)
}

Node.insertBefore() 方法在参考节点之前插入一个拥有指定父节点的子节点。如果给定的子节点是对文档中现有节点的引用,insertBefore() 会将其从当前位置移动到新位置(在将节点附加到其他节点之前,不需要从其父节点删除该节点)。

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
class VNode {
// 标签
tag: string | void;
// elm(Element)的属性
data: VNodeData | void;
// 子虚拟节点
children: ?Array<VNode>;
text: string | void;
// 真实dom元素
elm: Node | void;
// 元素命名空间
ns: string | void;
context: Component | void; // rendered in this component's scope
key: string | number | void;
componentOptions: VNodeComponentOptions | void;
componentInstance: Component | void; // component instance
parent: VNode | void; // component placeholder node

// strictly internal
raw: boolean; // contains raw HTML? (server only)
isStatic: boolean; // hoisted static node
isRootInsert: boolean; // necessary for enter transition check
isComment: boolean; // empty comment placeholder?
isCloned: boolean; // is a cloned node?
isOnce: boolean; // is a v-once node?
asyncFactory: Function | void; // async component factory function
asyncMeta: Object | void;
isAsyncPlaceholder: boolean;
ssrContext: Object | void;
fnContext: Component | void; // real context vm for functional nodes
fnOptions: ?ComponentOptions; // for SSR caching
devtoolsMeta: ?Object; // used to store functional render context for devtools
fnScopeId: ?string; // functional scope id support
}