Skip to content

源码笔记(九):update 阶段(下)

接上文,得到 vnode 后执行 vm._update。因为 vm._vnode 存在即已经渲染过,则走更新方法:

js
// updates
vm.$el = vm.__patch__(prevVnode, vnode);

vm.__patch__patch 方法,进入 patch 流程。

sameVnode

js
function sameVnode(a, b) {
  return (
    a.key === b.key &&
    ((a.tag === b.tag && a.isComment === b.isComment && isDef(a.data) === isDef(b.data) && sameInputType(a, b)) ||
      (isTrue(a.isAsyncPlaceholder) && a.asyncFactory === b.asyncFactory && isUndef(b.asyncFactory.error)))
  );
}

判断是否相同,则判断 key、 tag、是否有 data 的存在(不关心内部具体的值)、是否是注释节点、是否是相同的 input type,异步则判断是否有 相同的占位符,asyncFactory 等。若相同则视为同一节点进行 patch

patch

patch 的主要功能是将新旧 vnode 进行同级比较,然后更新真实 dom 节点和组件实例。

patch 源码

js
return function patch(oldVnode, vnode, hydrating, removeOnly) {
  if (isUndef(vnode)) {
    if (isDef(oldVnode)) {
      invokeDestroyHook(oldVnode);
    }

    return;
  }

  var isInitialPatch = false;
  var insertedVnodeQueue = [];

  if (isUndef(oldVnode)) {
    // empty mount (likely as component), create new root element
    isInitialPatch = true;
    createElm(vnode, insertedVnodeQueue);
  } else {
    var isRealElement = isDef(oldVnode.nodeType);

    if (!isRealElement && sameVnode(oldVnode, vnode)) {
      // patch existing root node
      patchVnode(oldVnode, vnode, insertedVnodeQueue, null, null, removeOnly);
    } else {
      if (isRealElement) {
        // 服务端渲染相关处理
        if (oldVnode.nodeType === 1 && oldVnode.hasAttribute(SSR_ATTR)) {
          oldVnode.removeAttribute(SSR_ATTR);
          hydrating = true;
        }
        // 服务端渲染相关处理
        if (isTrue(hydrating)) {
          if (hydrate(oldVnode, vnode, insertedVnodeQueue)) {
            invokeInsertHook(vnode, insertedVnodeQueue, true);
            return oldVnode;
          } else if (true) {
            warn(
              'The client-side rendered virtual DOM tree is not matching ' +
                'server-rendered content. This is likely caused by incorrect ' +
                'HTML markup, for example nesting block-level elements inside ' +
                '<p>, or missing <tbody>. Bailing hydration and performing ' +
                'full client-side render.'
            );
          }
        }

        oldVnode = emptyNodeAt(oldVnode);
      } // replacing existing element

      var oldElm = oldVnode.elm;
      var parentElm = nodeOps.parentNode(oldElm); // create new node

      createElm(vnode, insertedVnodeQueue, oldElm._leaveCb ? null : parentElm, nodeOps.nextSibling(oldElm));

      if (isDef(vnode.parent)) {
        var ancestor = vnode.parent;
        var patchable = isPatchable(vnode);

        while (ancestor) {
          for (var i = 0; i < cbs.destroy.length; ++i) {
            cbs.destroy[i](ancestor);
          }

          ancestor.elm = vnode.elm;

          if (patchable) {
            for (var i$1 = 0; i$1 < cbs.create.length; ++i$1) {
              cbs.create[i$1](emptyNode, ancestor);
            }

            var insert = ancestor.data.hook.insert;

            if (insert.merged) {
              // start at index 1 to avoid re-invoking component mounted hook
              for (var i$2 = 1; i$2 < insert.fns.length; i$2++) {
                insert.fns[i$2]();
              }
            }
          } else {
            registerRef(ancestor);
          }

          ancestor = ancestor.parent;
        }
      }

      if (isDef(parentElm)) {
        removeVnodes([oldVnode], 0, 0);
      } else if (isDef(oldVnode.tag)) {
        invokeDestroyHook(oldVnode);
      }
    }
  }

  invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch);
  return vnode.elm;
};

patch 逻辑

  • vnode 不存在
    • oldVnode 存在,则触发 invodeDestoryHook 进行销毁旧的节点(组件销毁用到)
  • vnode 存在
    • oldVnode 不存在,则调用 createElm 创建新的节点
    • oldVnode 存在
      • oldVnode 不是真实节点且和 vnode 是相同节点(调用 sameVnode 比较),则调用 patchVnode 进行该 vnode 的补丁操作
      • oldVnode 是真实节点,略过 SSR 相关,则先把真实节点转为空的 vnode。再调用 createElm 创建新的 DOM 节点并插入到真实的父节点中。如果 vnode 是组件实例,则递归更新各级父占位符节点(组件节点)的属性
        • oldVnode 的真实父 dom 节点存在,则调用 removeVnodes 将旧的节点从父节点中移除
        • oldVnode 的真实父 dom 节点不存在,则触发 invodeDestoryHook 进行销毁旧的节点
    • 触发 invokeInsertHook 并 返回 vnode.elm 真实 dom 节点

patchVnode

比较新旧 vnode 节点,根据不同的状态对 dom 做更新操作(添加,移动,删除)(属性更新,文本更新,子节点更新)并依次调用 prepatch, update, postpatch 等钩子。在 patchupdateChildren 里调用。

在编译阶段生成的一些静态子树,在这个过程 oldVnode 中由于不会改变而直接跳过比对,动态子树在比较过程中比较核心的部分就是当新旧 vnode 同时存在 children,通过 updateChildren 方法对子节点做更新。

patchVnode 为更新操作核心。

patchVnode 源码

js
function patchVnode(oldVnode, vnode, insertedVnodeQueue, ownerArray, index, removeOnly) {
  if (oldVnode === vnode) {
    return;
  }

  if (isDef(vnode.elm) && isDef(ownerArray)) {
    // clone reused vnode
    vnode = ownerArray[index] = cloneVNode(vnode);
  }

  var elm = (vnode.elm = oldVnode.elm);

  if (isTrue(oldVnode.isAsyncPlaceholder)) {
    if (isDef(vnode.asyncFactory.resolved)) {
      hydrate(oldVnode.elm, vnode, insertedVnodeQueue);
    } else {
      vnode.isAsyncPlaceholder = true;
    }

    return;
  }

  if (isTrue(vnode.isStatic) && isTrue(oldVnode.isStatic) && vnode.key === oldVnode.key && (isTrue(vnode.isCloned) || isTrue(vnode.isOnce))) {
    vnode.componentInstance = oldVnode.componentInstance;
    return;
  }

  var i;
  var data = vnode.data;

  if (isDef(data) && isDef((i = data.hook)) && isDef((i = i.prepatch))) {
    i(oldVnode, vnode);
  }

  var oldCh = oldVnode.children;
  var ch = vnode.children;

  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)) {
    if (isDef(oldCh) && isDef(ch)) {
      if (oldCh !== ch) {
        updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly);
      }
    } else if (isDef(ch)) {
      if (true) {
        checkDuplicateKeys(ch);
      }

      if (isDef(oldVnode.text)) {
        nodeOps.setTextContent(elm, '');
      }

      addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue);
    } else if (isDef(oldCh)) {
      removeVnodes(oldCh, 0, oldCh.length - 1);
    } else if (isDef(oldVnode.text)) {
      nodeOps.setTextContent(elm, '');
    }
  } else if (oldVnode.text !== vnode.text) {
    nodeOps.setTextContent(elm, vnode.text);
  }

  if (isDef(data)) {
    if (isDef((i = data.hook)) && isDef((i = i.postpatch))) {
      i(oldVnode, vnode);
    }
  }
}

patchVnode 逻辑

  • vnodeoldVnode 完全一致,直接返回
  • 异步占位则执行 hydrate 方法或者定义 isAsyncPlaceholdertrue,直接返回
  • oldVnodevnode 都是静态节点且相同,则 oldVnode.componentInstance 赋给 vnode.componentInstance,直接返回
  • vnodedata 属性,有 prepatch 则执行其 data.hook.prepatch 钩子更新子组件,并执行更新其属性(cbs.update);有 update 则执行其 data.hook.update 钩子
  • vnode 不是文本节点
    • vnodeoldVnodechildren 都存在且不完全相等,则调用 updateChildren 更新子节点
    • 只有 vnode 存在子节点,检查子节点 key 值后,如果 oldVnode 文本节点存在,则置为空。然后调用 addVnodes 添加这些子节点
    • 只有 oldVnode 存在子节点,则调用 removeVnodes 移除这些子节点
    • oldVnodevnode 都不存在子节点,但是 oldVnode 为文本节点或注释节点,则把 elm 的文本内容置为空
  • vnode 是文本节点或注释节点且 vnode.textoldVnode.text 不相等,则更新 elm 的文本内容为 vnode.text
  • vnode 为组件节点, 则执行其 data.hook.postpatch 钩子

updateChildren

diff 算法核心,仅在同级的 vnode 间做 diff,递归地进行同级 vnodediff,最终实现整个 DOM 树的更新。

updateChildren 源码

js
function updateChildren(parentElm, oldCh, newCh, insertedVnodeQueue, removeOnly) {
  var oldStartIdx = 0;
  var newStartIdx = 0;
  var oldEndIdx = oldCh.length - 1;
  var oldStartVnode = oldCh[0];
  var oldEndVnode = oldCh[oldEndIdx];
  var newEndIdx = newCh.length - 1;
  var newStartVnode = newCh[0];
  var newEndVnode = newCh[newEndIdx];
  var oldKeyToIdx, idxInOld, vnodeToMove, refElm;
  var canMove = !removeOnly;

  if (true) {
    checkDuplicateKeys(newCh);
  }

  while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
    if (isUndef(oldStartVnode)) {
      oldStartVnode = oldCh[++oldStartIdx]; // Vnode has been moved left
    } else if (isUndef(oldEndVnode)) {
      oldEndVnode = oldCh[--oldEndIdx];
    } else if (sameVnode(oldStartVnode, newStartVnode)) {
      patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx);
      oldStartVnode = oldCh[++oldStartIdx];
      newStartVnode = newCh[++newStartIdx];
    } else if (sameVnode(oldEndVnode, newEndVnode)) {
      patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue, newCh, newEndIdx);
      oldEndVnode = oldCh[--oldEndIdx];
      newEndVnode = newCh[--newEndIdx];
    } else if (sameVnode(oldStartVnode, newEndVnode)) {
      // Vnode moved right
      patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue, newCh, newEndIdx);
      canMove && nodeOps.insertBefore(parentElm, oldStartVnode.elm, nodeOps.nextSibling(oldEndVnode.elm));
      oldStartVnode = oldCh[++oldStartIdx];
      newEndVnode = newCh[--newEndIdx];
    } else if (sameVnode(oldEndVnode, newStartVnode)) {
      // Vnode moved left
      patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx);
      canMove && nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm);
      oldEndVnode = oldCh[--oldEndIdx];
      newStartVnode = newCh[++newStartIdx];
    } else {
      if (isUndef(oldKeyToIdx)) {
        oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx);
      }

      idxInOld = isDef(newStartVnode.key) ? oldKeyToIdx[newStartVnode.key] : findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx);

      if (isUndef(idxInOld)) {
        // New element
        createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx);
      } else {
        vnodeToMove = oldCh[idxInOld];

        if (sameVnode(vnodeToMove, newStartVnode)) {
          patchVnode(vnodeToMove, newStartVnode, insertedVnodeQueue, newCh, newStartIdx);
          oldCh[idxInOld] = undefined;
          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) {
    refElm = isUndef(newCh[newEndIdx + 1]) ? null : newCh[newEndIdx + 1].elm;
    addVnodes(parentElm, refElm, newCh, newStartIdx, newEndIdx, insertedVnodeQueue);
  } else if (newStartIdx > newEndIdx) {
    removeVnodes(oldCh, oldStartIdx, oldEndIdx);
  }
}

updateChildren 逻辑

执行 while 循环:

  1. 如果 oldStartVnode 不存在,则将 oldStartVnode 设置为下一个节点
  2. 如果 oldEndVnode 不存在,则将 oldEndVnode 设置为上一个节点
  3. 如果 oldStartVnodenewStartVnode 是同一节点,则调用 patchVnode 并将 oldStartVnodenewStartVnode 设置为下一个节点
  4. 如果 oldEndVnodenewEndVnode 是同一节点,则调用 patchVnode 并将 oldEndVnodenewEndVnode 设置为上一个节点
  5. 如果 oldStartVnodenewEndVnode 是同一节点,则调用 patchVnode,然后调用 insertBeforeoldStartVnode.elm 移动到 oldEndVnode.elm 之后,并将 oldStartVnode 设置为下一个节点,newEndVnode 设置为上一个节点
  6. 如果 oldEndVnodenewStartVnode 是同一节点,则调用 patchVnode,然后调用 insertBeforeoldEndVnode.elm 移动到 oldStartVnode.elm 之前,并将 oldEndVnode 设置为上一个节点,newStartVnode 设置为下一个节点
  7. 以上都未命中
    1. 如果 newStartVnodekey 值,则尝试在 oldChildren 中查找与 newStartVnode 具有相同 key 的节点的索引位置
    2. 如果 newStartVnode 没有 key 值,则尝试在 oldChildren 中查找与 newStartVnode 相同节点的索引位置
    3. 如果不存在索引,则调用 createElm 创建一个新节点插入到 oldStartVnode.elm 之前
    4. 如果存在索引
      1. 如果索引对应的节点与 newStartVnode 是相同节点,则调用 patchVnode 并将找到的节点置为 undefined, 然后调用 insertBefore 将该节点的 elm 移动到 oldStartVnode.elm 之前
      2. 如果不是,则调用 createElm 创建一个新的节点插入到 oldStartVnode.elm 之前
    5. newStartVnode 设置为下一个节点

oldChildrennewChildren 节点在 while 过程中如果任意一个的开始索引和结束索引重合,则表明遍历结束。

遍历结束后:

  1. 如果 oldStartIdx 大于 oldEndIdx,说明 newChildren 长度大于 oldChildren,则需要调用 addVnodes 添加 newStartIdxnewEndIdx 之间的节点
  2. 如果 newStartIdx 大于 newEndIdx,说明 oldChildren 长度大于 newChildren,则需要调用 removeVnodes 移除 oldStartIdxoldEndIdx 之间的节点

updateChildren 例子

以旧 Vnode 节点为 A,B,C,D,F,新 Vnode 节点为 E,D,A,C,B,以()代表 startVnode{}代表 endVnode,则 updateChildren 过程为:

第一次循环第二次循环第三次循环第四次循环第五次循环跳出循环
旧 Vnode(A),B,C,D,(A),B,C,D,(A),B,C,undef,A,(B),C,undef,A,B,(C),undef,A,B,C,(undef),
新 Vnode(E),D,A,C,E,(D),A,C,E,D,(A),C,E,D,A,(C),E,D,A,{(C)},BE,D,A,{C},(B)
得到的真实 DomE,A,B,C,D,FE,D,A,B,C,FE,D,A,B,C,FE,D,A,C,F,BE,D,A,C,F,BE,D,A,C,B

updateChildren 注意点

  • 设置 4 指针并向中间靠拢的目的,是因为在前端需求场景中,大概率操作是在列表开头/结尾增加/删除了一个元素,减少循环,提高效率
  • 设置 key 可以直接查找匹配的节点是否相同,提高效率
  • 首尾互相比较和 key 比较都未查到相同则暴力循环查相同节点
  • 整个操作都是在通过比较 vnode 来操作真实 domvnode 在大多数情况下都不变

为什么不用 index 作为 key

由源码可知,当用 index 作为 key 时,则新旧 vnodekey 都一样,则直接命中 updateChildren 逻辑 的第 3 条进行繁琐的 patchVnode,而不会命中后面真正相同节点进行复用操作的逻辑;

除了性能损耗外,如果该 vnode 里有子节点且其 props 未变化(依赖子组件状态或临时 DOM 状态),则将不会更新子组件,则节点不更新从而引起错误。

子组件更新

对于本例,在 updateChildren 里,根据新旧 vnode 的差异,递归找到差异节点进行 patch,不再详述。

除此之外,对于第一个组件节点 App,在 patchVnode 里先执行 prepatch 钩子时,执行 updateChildComponent 更新子组件实例。updateChildComponent 里会对组件实例进行一系列的更新,包括 vm.$vnode 的更新、slot 的更新、listeners 的更新、props 的更新等等。如果有 props 且值有更新(本 demonum 由 28 更新到 29):

js
props[key] = validateProp(key, propOptions, propsData, vm);

则会重新给 num 赋值并触发对应的订阅列表,将 app 组件的 渲染 watcher 推入 queue 队列,在当前 watcher 执行完成后在执行该 watcher 的更新。

当前 Vue 即根组件 watcher 执行完成即视图更新后,回到 flushSchedulerQueue 里执行刚推入 queue 的第三个 watcherApp 组件的 渲染 watcher)开始进行子组件的更新。

子组件视图更新结束后,回到 flushSchedulerQueue 执行 callUpdatedHooks 钩子,以倒序方式执行 queue 队列里各 渲染 watcher 对应的组件的 updated 钩子,本例中为 App one updated->vue updated

到此,vue 更新结束,整体生命周期为:vue beforeUpdate -> App one beforeUpdate -> App one updated -> vue updated

本章小结

  1. 本章主要介绍了在得到新 vnode 后,如何与旧 vnode 进行最小化差异更新真实 dom
  2. 更新 dom 涉及到 3 个方法:patch,patchVnode,updateChildren。其中 updateChildrendiff 算法核心。
  3. 对于子组件更新执行 updateChildComponent 方法,若 props 有变化则会重新渲染子组件。根据 slot 情况可能涉及到强制更新组件。