vue3.0 中的 diff 过程

16 篇文章 1 订阅

简单的学习一下 vue3.0 中的 diff 过程

1、patchChildren,diff 函数的入口

const patchChildren = (n1, n2, container, anchor, parentComponent, parentSuspense, isSVG, optimized = false) => {
          const c1 = n1 && n1.children;
          const prevShapeFlag = n1 ? n1.shapeFlag : 0;
          const c2 = n2.children;
          const { patchFlag, shapeFlag } = n2;
          if (patchFlag === -2 /* BAIL */) {
              optimized = false;
          }
          // fast path
        //   这个 patchFlag 是在 编译的时候 生成的,对于节点来说,一般都是 0
          if (patchFlag > 0) {
              if (patchFlag & 128 /* KEYED_FRAGMENT */) {
                  // this could be either fully-keyed or mixed (some keyed some not)
                  // presence of patchFlag means children are guaranteed to be arrays
                  patchKeyedChildren(c1, c2, container, anchor, parentComponent, parentSuspense, isSVG, optimized);
                  return;
              }
              else if (patchFlag & 256 /* UNKEYED_FRAGMENT */) {
                  // unkeyed
                  patchUnkeyedChildren(c1, c2, container, anchor, parentComponent, parentSuspense, isSVG, optimized);
                  return;
              }
          }
          // children has 3 possibilities: text, array or no children.
        //   子节点有三种可能:文本子节点,数组子节点,或者没有子节点
          if (shapeFlag & 8 /* TEXT_CHILDREN */) {  // 当前的子节点是 文本节点的时候
              // text children fast path
              if (prevShapeFlag & 16 /* ARRAY_CHILDREN */) {  // 如果之前的子节点是 数组的话,就直接把之前的节点 给清除
                  unmountChildren(c1, parentComponent, parentSuspense);
              }
              if (c2 !== c1) {  // 如果 之前的子节点 和新的子节点不相同,直接使用 新的 文本替换以前的子节点
                  hostSetElementText(container, c2);
              }
          }
          else {
              if (prevShapeFlag & 16 /* ARRAY_CHILDREN */) {  // 如果之前的节点是 数组节点的话
                  // prev children was array
                  if (shapeFlag & 16 /* ARRAY_CHILDREN */) {  // 如果当前节点也是 数组节点的话,就进入 diff 的过程
                      // two arrays, cannot assume anything, do full diff
                      patchKeyedChildren(c1, c2, container, anchor, parentComponent, parentSuspense, isSVG, optimized);
                  }
                  else { // 如果 当前的节点 不是 文本节点,也不是数组节点,那么就是空节点,删除 老的 节点
                      // no new children, just unmount old
                      unmountChildren(c1, parentComponent, parentSuspense, true);
                  }
              }
              // 现在的情况就是 之前的节点 要么是 文本节点,或者为空
              // 而新的节点 要么是 数组,要么是空
              else {
                //   新的节点是 数组 或者 为空节点
                  if (prevShapeFlag & 8 /* TEXT_CHILDREN */) { // 如果 之前的 节点是文本节点,那么 直接清除 
                      hostSetElementText(container, '');
                  }
                  // mount new if array
                  if (shapeFlag & 16 /* ARRAY_CHILDREN */) { // 现在以前的子节点已经是空了,如果新的节点是数组,生成新的节点
                      mountChildren(c2, container, anchor, parentComponent, parentSuspense, isSVG, optimized);
                  }
              }
          }
      };
  1. 获取子节点和子节点的类型,新节点  n2 和 旧节点 n1 中,一共有三种类型的子节点:文本节点,数组节点,空节点
  2. 如果 新节点是 文本节点,而 老节点 是数组节点的话,就删除老姐点,这样老节点就是空,然后插入文本节点
  3. 如果 新节点是 文本节点,老节点 是文本节点,比较新旧节点 文本 是否一致,否就 替换文本内容
  4. 如果 老节点是数组节点,而新节点也是数组节点的话,进入 patchKeyedChildren ,也就是 diff 的过程
  5. 如果 老节点是数组节点,而新节点是空节点 的话,进入 删除老节点
  6. 这样比较下来,剩下的情况就是 : 之前的节点 要么是 文本节点,或者为空,而新的节点 要么是 数组,要么是空
  7. 所以如果之前的节点是文本节点删除 老节点的文本内容
  8. 如果 新节点是数组,就把新节点添加到 dom 树上去
  9. 最后就剩下新节点是空节点不做任何操作(在第7点已经把老节点删除了)

2、diff 中的辅助函数

2.1 normalizeVNode 创建 vnode

    function normalizeVNode(child) {
      // 如果 当前节点为空,创建注释 vnode
      if (child == null || typeof child === 'boolean') {
          // empty placeholder
          return createVNode(Comment);
      }
      // 如果 当前节点 是数组,创建 一个 Fragment 节点,并把子节点 传入
      else if (isArray(child)) {
          // fragment
          return createVNode(Fragment, null, child);
      }
      // 如果当前节点是一个 对象,那么 就是 vnode
      else if (typeof child === 'object') {
         // 如果存在 el属性,说明已经 经历了 从 vnode 转变为 element 挂载的过程,就克隆一份
          return child.el === null ? child : cloneVNode(child);
      }
      // 最后就是一个 基础类型的节点了,数字 或者 文本
      else {
          return createVNode(Text, null, String(child));
      }
  }

2.2 cloneIfMounted 如果当前 vnode 挂载过了,就克隆一份

  function cloneIfMounted(child) {
     // 如果存在 el属性,说明已经 经历了 从 vnode 转变为 element 挂载的过程,就克隆一份
      return child.el === null ? child : cloneVNode(child);
  }

2.3 isSameVNodeType 是否是 相同的 vnode 节点

  function isSameVNodeType(n1, n2) {
      return n1.type === n2.type && n1.key === n2.key;
  }

这里 的 是否是 相同节点对比于以前是有了很大的变化的,偏向于 react 的方向变化,看下面的 代码就是以前Vue2.x的 sameNode

// 两个 vnode 是否相同
function sameVnode(a, b) {
  return (
    // 需要注意的是 null === null 为 true
    // 也就是说,如果 没有设置 key 的话,那么就默认为 两个节点的key 是一致的
    // 对比于 react 的diff ,这里的判断 多出了 tag、comment 和 input 的判断
    // 而 react 则着重于 $$type 的判断
    // 这里 react 明显的 对于 jsx 的分类 更倾向于 自己的判断,而 vue 则是 倾向于 用户的输入
    a.key === b.key && (
      (
        a.tag === b.tag &&
        a.isComment === b.isComment &&
        isDef(a.data) === isDef(b.data) &&
        // 是否是 同一个类型的 input 标签,这个很好理解
        sameInputType(a, b)
      ) || (
        isTrue(a.isAsyncPlaceholder) &&
        a.asyncFactory === b.asyncFactory &&
        isUndef(b.asyncFactory.error)
      )
    )
  )
}
// 是否是 相同类型的 input 节点,比如 radio,text,checkbox等
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)
}

3. patchKeyedChildren diff 的过程

const patchKeyedChildren = (c1, c2, container, parentAnchor, parentComponent, parentSuspense, isSVG, optimized) => {
          let i = 0;
        //   新子节点的长度
          const l2 = c2.length;
        //   旧子节点的 排尾
          let e1 = c1.length - 1; // prev ending index
        //   薪子节点的 排尾
          let e2 = l2 - 1; // next ending index
        //   从头开始 遍历 新旧节点
        // 首先 获取 当前 index 下的子节点,然后 对新节点 获取 对应的 vnode
          while (i <= e1 && i <= e2) {
              const n1 = c1[i];
              const n2 = (c2[i] = optimized
                  ? cloneIfMounted(c2[i])  // 如果新节点经历了挂载的阶段,就克隆一份
                  : normalizeVNode(c2[i]));  // 否则就创建一个 新节点的 vnode
                // 如果是相同的节点,也就是 type 和 key 相同
              if (isSameVNodeType(n1, n2)) {
                  patch(n1, n2, container, parentAnchor, parentComponent, parentSuspense, isSVG, optimized);
              }
              else {
                //   如果有不同的,立刻结束循环
                  break;
              }
              i++;
          }
        //   从尾遍历 新旧节点,过程和上面几乎一样
          while (i <= e1 && i <= e2) {
              const n1 = c1[e1];
              const n2 = (c2[e2] = optimized
                  ? cloneIfMounted(c2[e2])
                  : normalizeVNode(c2[e2]));
              if (isSameVNodeType(n1, n2)) {
                  patch(n1, n2, container, parentAnchor, parentComponent, parentSuspense, isSVG, optimized);
              }
              else {
                  break;
              }
              e1--;
              e2--;
          }
          // 3. common sequence + mount
          // (a b)
          // (a b) c
          // i = 2, e1 = 1, e2 = 2
          // (a b)
          // c (a b)
          // i = 0, e1 = -1, e2 = 0
        // 遇到 类似于 旧节点为空,而新节点 依旧存在值 的情况下,
        //  也就是 旧节点 比较完毕了
        //   插入新节点
          if (i > e1) {
              if (i <= e2) {
                  const nextPos = e2 + 1;
                  const anchor = nextPos < l2 ? c2[nextPos].el : parentAnchor;
                  while (i <= e2) {
                      patch(null, (c2[i] = optimized
                          ? cloneIfMounted(c2[i])
                          : normalizeVNode(c2[i])), container, anchor, parentComponent, parentSuspense, isSVG);
                      i++;
                  }
              }
          }
          // 4. common sequence + unmount
          // (a b) c
          // (a b)
          // i = 2, e1 = 2, e2 = 1
          // a (b c)
          // (b c)
          // i = 0, e1 = 0, e2 = -1
        //   新节点比较完毕了
        //   删除 多余的 旧节点
          else if (i > e2) {
              while (i <= e1) {
                  unmount(c1[i], parentComponent, parentSuspense, true);
                  i++;
              }
          }
          // 5. unknown sequence
          // [i ... e1 + 1]: a b [c d e] f g
          // [i ... e2 + 1]: a b [e d c h] f g
          // i = 2, e1 = 4, e2 = 5
        //   最后,也就是 最普遍的情况,也就是 当前的 新旧节点都有剩余
          else {
              const s1 = i; // 旧节点比较的开始索引
              const s2 = i; // 新节点比较的开始索引
              const keyToNewIndexMap = new Map();
              for (i = s2; i <= e2; i++) {
                  const nextChild = (c2[i] = optimized
                      ? cloneIfMounted(c2[i])
                      : normalizeVNode(c2[i]));
                //  当 key 存在的时候,为 新节点 建立 一个 key 和 当前节点 index 对应关系的 Map
                  if (nextChild.key != null) {
                    //   如果 key 重复了,报个 错
                      if ( keyToNewIndexMap.has(nextChild.key)) {
                          warn(`Duplicate keys found during update:`, JSON.stringify(nextChild.key), `Make sure keys are unique.`);
                      }
                      keyToNewIndexMap.set(nextChild.key, i);
                  }
              }
              // 5.2 loop through old children left to be patched and try to patch
              // matching nodes & remove nodes that are no longer present
              let j;
              let patched = 0;
            // 新节点还有没有 遍历 到的长度
              const toBePatched = e2 - s2 + 1;
              let moved = false;
              // used to track whether any node has moved
            //   用来标记是否 有节点被移动了
              let maxNewIndexSoFar = 0;
                // 这个数组存储新子序列中的元素在旧子序列节点的索引,用于确定最长稳定子序列
                // <newIndex,oldIndex>
                // 遍历这个数组,填上默认字符 0
              const newIndexToOldIndexMap = new Array(toBePatched);
              for (i = 0; i < toBePatched; i++)
                  newIndexToOldIndexMap[i] = 0;
                //   遍历剩下的旧节点
              for (i = s1; i <= e1; i++) {
                  const prevChild = c1[i];
                //   所有的新节点都被 patch 了,这里就是把以前的节点删除
                  if (patched >= toBePatched) {
                      // all new children have been patched so this can only be a removal
                      unmount(prevChild, parentComponent, parentSuspense, true);
                      continue;
                  }
                  let newIndex;
                //   如果老的节点 存在 key
                  if (prevChild.key != null) {
                    //   在之前建立的 Map 中 获取 和老节点 的 key 相同的 index
                      newIndex = keyToNewIndexMap.get(prevChild.key);
                  }
                  else {
                      // key-less node, try to locate a key-less node of the same type
                    //   如果 没有设置 key 的话,就只能去遍历新节点 然后获取 相同类型,且没有设置 key 的新节点了
                      for (j = s2; j <= e2; j++) {
                          if (newIndexToOldIndexMap[j - s2] === 0 &&
                              isSameVNodeType(prevChild, c2[j])) {
                              newIndex = j;
                              break;
                          }
                      }
                  }
                //   如果不存在 可以复用的节点,就删除 当前 的老节点
                  if (newIndex === undefined) {
                      unmount(prevChild, parentComponent, parentSuspense, true);
                  }
                  else {
                      // 更新新子序列中的元素在旧子序列中的索引,这里加 1 偏移,是为了避免 i 为 0 的特殊情况
                      newIndexToOldIndexMap[newIndex - s2] = i + 1;
                    //   标记 移动的节点
                      if (newIndex >= maxNewIndexSoFar) {
                          maxNewIndexSoFar = newIndex;
                      }
                      else {
                          moved = true;
                      }
                    //   更新节点
                      patch(prevChild, c2[newIndex], container, null, parentComponent, parentSuspense, isSVG, optimized);
                      patched++;
                  }
              }
              // 5.3 move and mount
              // generate longest stable subsequence only when nodes have moved
            // 移动和挂载新节点
            // 仅当节点移动时生成最长上升子序列
              const increasingNewIndexSequence = moved
                  ? getSequence(newIndexToOldIndexMap)
                  : EMPTY_ARR;
              j = increasingNewIndexSequence.length - 1;
              // looping backwards so that we can use last patched node as anchor
              for (i = toBePatched - 1; i >= 0; i--) {
                  const nextIndex = s2 + i;
                  const nextChild = c2[nextIndex];
                  const anchor = nextIndex + 1 < l2 ? c2[nextIndex + 1].el : parentAnchor;
                   // 有移动的节点就会在 重新设定 偏移的 index,
                //    所以依旧为 0 的话,说明当前节点没有发生移动,就是没有在 旧节点 中找到对应的节点
                //  挂载 新节点
                  if (newIndexToOldIndexMap[i] === 0) { 
                      // mount new
                      patch(null, nextChild, container, anchor, parentComponent, parentSuspense, isSVG);
                  }
                //   如果需要移动的话
                  else if (moved) {
                      // move if:
                      // There is no stable subsequence (e.g. a reverse)
                      // OR current node is not among the stable sequence
                      // 或者当前没有最长上升子序列
                      // 当前的节点 不在 最长上升子序列当中
                      // 对节点进行移动  
                      if (j < 0 || i !== increasingNewIndexSequence[j]) {
                          move(nextChild, container, anchor, 2 /* REORDER */);
                      }
                      else {
                          j--;
                      }
                  }
              }
          }
      };
  1. 从头遍历新节点和旧节点如果有相同节点,更新节点,遇到第一个不一样的,结束循环
  2. 从尾遍历新节点和旧节点如果有相同节点,更新节点,遇到第一个不一样的,结束循环
  3. 如果旧节点 比较完毕了,插入新节点
  4. 如果新节点 比较完毕了,删除旧节点
  5. 给新节点 建立一个 <key, index> 的 Map 对象
  6. 使用 老节点 的key 来查找,如果存在 相同节点,就 patch
  7. 如果不存在 相同节点,把老节点删除
  8. 最后再  移动和挂载新节点(这一块有点没看懂,以后再研究研究)

4、总结

这里就总结一下  Vue 源码中 的 diff  和 vue3.0 的区别,以及部分 react 的 diff  的区别

  1. 首先 vue3.0 的 diff 很明显 向 react 的 diff 靠拢,取消了 vue2.0 中的 排头 和排尾 ,排尾 和排头的比较
  2. 比较 也是采用了 一个 循环遍历,遇到第一个不一样的就 退出循环
  3. 与 比较中 与 react 不同的,还多了一次 从后往前面的遍历,算是 vue2.0 的精华遗产了
  4. 然后 vue2.0 建立的 <key,index> 是一个 Object,而 react 和 vue3.0 中的是 Map ,所以现在 在 3.0 中使用 对象作为 key 也是可以的了(在 object 中 使用 对象作为 key 会自动变成 [object Object],所有 的key 都会重复)
  5. 最后执行  移动和挂载新节点

 

 

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值