携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第22天,点击查看活动详情
前言
上一篇文章了解到 DOM-Diff 算法,知道在 patch 过程中做了三件事:创建节点、删除节点、更新节点。最后在更新节点中,如果新旧 VNode 都包含子节点,还需要对子节点递归对比更新子节点。
分析更新情况
当新的VNode与旧的oldVNode都是元素节点并且都包含子节点时,那么这两个节点的VNode实例上的children属性就是所包含的子节点数组。新的VNode上的子节点数组记为newChildren,旧的oldVNode上的子节点数组记为oldChildren,把newChildren里面的元素与oldChildren里的元素一一进行对比,通过两层循环,每循环外层newChildren数组里的一个子节点,就去内层oldChildren数组里找看有没有与之相同的子节点,伪代码如下:
for (let i = 0; i < newChildren.length; i++) {
const newChild = newChildren[i];
for (let j = 0; j < oldChildren.length; j++) {
const oldChild = oldChildren[j];
if (newChild === oldChild) {
// ...
}
}
}
那么以上这个过程将会存在以下四种情况:
-
创建子节点
如果
newChildren有,oldChildren没有,那么就创建该子节点。 -
删除子节点
如果
newChildren里面的每一个子节点都循环完毕,oldChildren还有未处理的子节点,就将这些节点删除。 -
移动子节点
如果
newChildren某个子节点在oldChildren里找到了与之相同的子节点,但是所处的位置不同,以newChildren里子节点的位置为基准,调整oldChildren里该节点的位置,使之与在newChildren里的位置相同。 -
更新节点
如果
newChildren某个子节点在oldChildren里找到了与之相同的子节点,并且所处的位置也相同,那么就更新oldChildren里该节点,使之与newChildren里的该节点相同。
创建子节点
如果newChildren有,oldChildren没有,那么就创建该子节点。创建好之后再把它插入到DOM中合适的位置。
参考这篇文章vue源码解读--Vue中的DOM-Diff
创建成功后,在 oldVNode 中找到合适的位置并插入进去:
上图中左边是新的VNode,右边是旧的oldVNode,同时也是真实的DOM。当我们循环newChildren数组里面的子节点,前两个子节点都在oldChildren里找到了与之对应的子节点,处理过后把它们标志为已处理,当循环到newChildren数组里第三个子节点时,发现在oldChildren里找不到与之对应的子节点,那创建这个节点,创建好之后把它插入到已处理节点的后面。如果第四个子节点也没有在 oldVNode 中找到与之对应的子节点,那就会如下图也在已处理后面创建一个子节点,就会发生子节点的位置错误。
所以应该把新创建的节点插入到第一个未处理节点之前。这样不管有多少个新增的节点,位置才不会错。
插入的位置是第一个未处理节点之前,而并非最后一个已处理节点之后。
删除子节点
如果newChildren里面的每一个子节点都循环完毕,oldChildren还有未处理的子节点,就将这些节点删除。
参考这篇文章vue源码解读--Vue中的DOM-Diff
更新子节点
如果newChildren某个子节点在oldChildren里找到了与之相同的子节点,并且所处的位置也相同,那么就更新oldChildren里该节点,使之与newChildren里的该节点相同。
参考这篇文章vue源码解读--Vue中的DOM-Diff
移动子节点
如果newChildren某个子节点在oldChildren里找到了与之相同的子节点,但是所处的位置不同,以newChildren里子节点的位置为基准,调整oldChildren里该节点的位置,使之与在newChildren里的位置相同。
newChildren里面的第三个子节点与真实DOM即oldChildren里面的第四个子节点相同但是所处位置不同,应该以newChildren里子节点的位置为基准,调整oldChildren里该节点的位置,把oldChildren里面的第四个节点移动到第三个节点的位置,移动的位置是第一个未处理节点之前。
源码分析
OK,以上就是更新子节点时所要考虑的所有情况了,源码如下:
// 源码位置: /src/core/vdom/patch.js
if (isUndef(idxInOld)) {
// 如果在oldChildren里找不到当前循环的newChildren里的子节点
// 新增节点并插入到合适位置
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);
}
}
首先判断在oldChildren里能否找到当前循环的newChildren里的子节点,如果找不到,那就是新增节点并插入到合适位置;如果找到了,先对比两个节点是否相同,若相同则先调用patchVnode更新节点,更新完之后再看是否需要移动节点,注意,源码判断是否需要移动子节点时用了&&,下面这两种写法是等价的:
canMove && nodeOps.insertBefore(parentElm, vnodeToMove.elm, oldStartVnode.elm);
// 等同于
if (canMove) {
nodeOps.insertBefore(parentElm, vnodeToMove.elm, oldStartVnode.elm);
}
小结
- 分析更新字节点的情况,新的有,旧的没有,就创建;反之就删除;新的有,旧的也有,就更新;位置不同就移动。
- 根据每种情况深入分析,插入节点和移动位置怎么找合适的位置。
- 双层循环虽然能解决问题,但是如果节点数量多,时间复杂度就非常高,所以Vue还提供了一种优化算法。