vue源码解读--Vue更新子节点

269 阅读5分钟

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第22天,点击查看活动详情

参考自Vue源码系列-Vue中文社区

前言

上一篇文章了解到 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 中找到合适的位置并插入进去:

4.cb62f1aa.png

上图中左边是新的VNode,右边是旧的oldVNode,同时也是真实的DOM。当我们循环newChildren数组里面的子节点,前两个子节点都在oldChildren里找到了与之对应的子节点,处理过后把它们标志为已处理,当循环到newChildren数组里第三个子节点时,发现在oldChildren里找不到与之对应的子节点,那创建这个节点,创建好之后把它插入到已处理节点的后面。如果第四个子节点也没有在 oldVNode 中找到与之对应的子节点,那就会如下图也在已处理后面创建一个子节点,就会发生子节点的位置错误。

5.bcb4dcee.png

所以应该把新创建的节点插入到第一个未处理节点之前。这样不管有多少个新增的节点,位置才不会错。

插入的位置是第一个未处理节点之前,而并非最后一个已处理节点之后

删除子节点

如果newChildren里面的每一个子节点都循环完毕,oldChildren还有未处理的子节点,就将这些节点删除。

参考这篇文章vue源码解读--Vue中的DOM-Diff

更新子节点

如果newChildren某个子节点在oldChildren里找到了与之相同的子节点,并且所处的位置也相同,那么就更新oldChildren里该节点,使之与newChildren里的该节点相同。

参考这篇文章vue源码解读--Vue中的DOM-Diff

移动子节点

如果newChildren某个子节点在oldChildren里找到了与之相同的子节点,但是所处的位置不同,以newChildren里子节点的位置为基准,调整oldChildren里该节点的位置,使之与在newChildren里的位置相同。

6.b9621b4d.png newChildren里面的第三个子节点与真实DOMoldChildren里面的第四个子节点相同但是所处位置不同,应该以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);
}

小结

  1. 分析更新字节点的情况,新的有,旧的没有,就创建;反之就删除;新的有,旧的也有,就更新;位置不同就移动。
  2. 根据每种情况深入分析,插入节点和移动位置怎么找合适的位置。
  3. 双层循环虽然能解决问题,但是如果节点数量多,时间复杂度就非常高,所以Vue还提供了一种优化算法。