Vue2 Diff算法总结(图文结合)

Diff算法的目的

Diff算法的目的是为了找到哪些节点发生了变化,哪些节点没有发生变化可以复用。如果用最传统的

diff算法,如下图所示,每个节点都要遍历另⼀棵树上的所有节点做比较,这就是o(n^2)的复杂度,加

上更新节点时的o(n)复杂度,那就总共达到了o(n^3)的复杂度,这对于⼀个结构复杂节点数众多的页面,成本是非常大的。

所以实际上vue和react都对虚拟dom的diff算法做了优化了,将复杂度降低到了o(n)级别,具体的策略变为了:同层级的节点才能互相比较:

1. 节点比较时,如果类型不同,则对该节点及其所有子节点直接销毁新建;

2. 类型相同的字节点,使用key帮助查找,并且使用算法优化查找效率。其中react和vue2以及vue3

的diff算法都不尽相同;

前提:

• mount(vnode, parent, [refNode]) : 通过 vnode 生成真实的DOM节点。parent为其父

级的真实DOM节点, refNode 为真实的DOM节点,其父级节点为parent。如果refNode不为

空,vnode生成的DOM节点就会插⼊到refNode之前;如果refNode为空,那么vnode生成的DOM

节点就作为最后⼀个子节点插⼊到parent中

• patch(prevNode, nextNode, parent) : 可以简单的理解为给当前DOM节点进行更新,并

且调用diff算法对比自身的子节点;

双端比较

双端比较就是新列表和旧列表两个列表的头与尾互相对比,,在对比的过程中指针会逐渐向内靠拢,

直到某⼀个列表的节点全部遍历过,对比停止;(可以简单的理解对比头头,尾尾,头尾,尾头)

patch

  • 先判断是否是首次渲染,如果是首次渲染那么我们就直接createElm即可;如果不是就去判断新老两个 节点的元素类型否⼀样;如果两个节点都是⼀样的,那么就深入检查他们的子节点。如果两个节点不一样那就说明Vnode完全被改变了,就可以直接替换oldVnode;

patch源码如下:

function patch(oldVnode, vnode, hydrating, removeOnly) {
  // 判断新的vnode是否为空
  if (isUndef(vnode)) {
    // 如果⽼的vnode不为空 卸载所有的⽼vnode
    if (isDef(oldVnode)) invokeDestroyHook(oldVnode)
    return
  }
  let isInitialPatch = false
  // ⽤来存储 insert钩⼦函数,在插⼊节点之前调⽤
  const insertedVnodeQueue = []
  // 如果⽼节点不存在,直接创建新节点
  if (isUndef(oldVnode)) {
    isInitialPatch = true
    createElm(vnode, insertedVnodeQueue)
  } else {
    // 是不是元素节点
    const isRealElement = isDef(oldVnode.nodeType)
    // 当⽼节点不是真实的DOM节点,并且新⽼节点的type和key相同,进⾏patchVnode更新⼯作
    if (!isRealElement && sameVnode(oldVnode, vnode)) {
      patchVnode(oldVnode, vnode, insertedVnodeQueue, null, null, removeOnly)
    } else {
      // 如果不是同⼀元素节点的话
      // 当⽼节点是真实DOM节点的时候
      if (isRealElement) {
        // 如果是元素节点 并且在SSR环境的时候 修改SSR_ATTR属性
        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
          }
        }
        // 如果不是服务端渲染的,或者混合失败,就创建⼀个空的注释节点替换 oldVnode
        oldVnode = emptyNodeAt(oldVnode)
      }

      // 拿到 oldVnode 的⽗节点
      const oldElm = oldVnode.elm
      const parentElm = nodeOps.parentNode(oldElm)

      // 根据新的 vnode 创建⼀个 DOM 节点,挂载到⽗节点上
      createElm(
        vnode,
        insertedVnodeQueue,
        oldElm._leaveCb ? null : parentElm,
        nodeOps.nextSibling(oldElm)
      )
      // 如果新的 vnode 的根节点存在,就是说根节点被修改了,就需要遍历更新⽗节点
      // 递归 更新⽗占位符元素
      // 就是执⾏⼀遍 ⽗节点的 destory 和 create 、insert 的 钩⼦函数
      if (isDef(vnode.parent)) {
        let ancestor = vnode.parent
        const patchable = isPatchable(vnode)
        // 更新⽗组件的占位元素
        while (ancestor) {
          // 卸载⽼根节点下的全部组件
          for (let i = 0; i < cbs.destroy.length; ++i) {
            cbs.destroy[i](ancestor)
          }
          // 替换现有元素
          ancestor.elm = vnode.elm
          if (patchable) {
            for (let i = 0; i < cbs.create.length; ++i) {
              cbs.create[i](emptyNode, ancestor)
            }
            // #6513
            // invoke insert hooks that may have been merged by create hooks.
            // e.g. for directives that uses the "inserted" hook.
            const insert = ancestor.data.hook.insert
            if (insert.merged) {
              // start at index 1 to avoid re-invoking component mounted hook
              for (let i = 1; i < insert.fns.length; i++) {
                insert.fns[i]()
              }
            }
          } else {
            registerRef(ancestor)
          }
          // 更新⽗节点
          ancestor = ancestor.parent
        }
      }
      // 如果旧节点还存在,就删掉旧节点
      if (isDef(parentElm)) {
        removeVnodes([oldVnode], 0, 0)
      } else if (isDef(oldVnode.tag)) {
        // 否则直接卸载 oldVnode
        invokeDestroyHook(oldVnode)
      }
    }
  }
  // 执⾏ 虚拟 dom 的 insert 钩⼦函数
  invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch)
  // 返回最新 vnode 的 elm ,也就是真实的 dom节点
  return vnode.elm
}

patchVnode

• 如果 Vnode 和 oldVnode 指向同⼀个对象,则直接return即可;

• 将旧节点的真实 DOM 赋值到新节点(真实 dom 连线到新⼦节点)称为elm,然后遍历调⽤

update 更新 oldVnode 上的所有属性,⽐如 class,style,attrs,domProps,events...;

• 如果新⽼节点都有⽂本节点,并且⽂本不相同,那么就⽤ vnode .text更新⽂本内容。

• 如果oldVnode有⼦节点⽽ Vnode 没有,则直接删除⽼节点即可;

• 如果oldVnode没有⼦节点⽽ Vnode 有,则将Vnode的⼦节点真实化之后添加到DOM中即可。

• 如果两者都有⼦节点,则执⾏ updateChildren 函数⽐较⼦节点。

patchVnode的源码如下:

function patchVnode(
  oldVnode, // 旧的虚拟 DOM 节点
  vnode, // 新节点
  insertedVnodeQueue, // 插入节点队列
  ownerArray, // 节点数组
  index, // 当前节点的下标
  removeOnly
) {
  // 新旧节点的地址一样,直接跳过
  if (oldVnode === vnode) {
    return;
  }

  // 如果新节点有elm属性,并且ownerArray也有定义,说明是复用的节点,进行克隆
  if (isDef(vnode.elm) && isDef(ownerArray)) {
    vnode = ownerArray[index] = cloneVNode(vnode);
  }

  const elm = vnode.elm = oldVnode.elm;

  // 如果当前节点是注释或v-if的,或者是异步函数,就跳过检查异步组件
  if (isTrue(oldVnode.isAsyncPlaceholder)) {
    if (isDef(vnode.asyncFactory.resolved)) {
      // 执行hydrate,将新节点添加到旧节点中
      hydrate(oldVnode.elm, vnode, insertedVnodeQueue);
    } else {
      vnode.isAsyncPlaceholder = true;
    }
    return;
  }

  // 当前节点是静态节点的时候,key也一样,或者有v-once的时候,就直接赋值返回
  if (
    isTrue(vnode.isStatic) &&
    isTrue(oldVnode.isStatic) &&
    vnode.key === oldVnode.key &&
    (isTrue(vnode.isCloned) || isTrue(vnode.isOnce))
  ) {
    vnode.componentInstance = oldVnode.componentInstance;
    return;
  }

  let i;
  const data = vnode.data;

  if (isDef(data) && isDef(i = data.hook) && isDef(i = i.prepatch)) {
    // 调用prepatch钩子函数
    i(oldVnode, vnode);
  }

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

  if (isDef(data) && isPatchable(vnode)) {
    // 遍历调用update更新oldVnode所有属性,比如class、style、attrs、domProps、eventListeners等
    // 这⾥的 update 钩⼦函数是 vnode 本⾝的钩⼦函数
    for (i = 0; i < cbs.update.length; ++i) cbs.update[i](oldVnode, vnode);
    // 这⾥的 update 钩⼦函数是我们传过来的函数
    // 调用用户自定义的update钩子函数
    if (isDef(i = data.hook) && isDef(i = i.update)) i(oldVnode, vnode);
  }

  // 如果新节点不是文本节点,也就是说有子节点
  if (isUndef(vnode.text)) {
    if (isDef(oldCh) && isDef(ch)) {
      // 如果新旧节点的子节点不一样,执行updateChildren函数,对比子节点
      if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly);
    } else if (isDef(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)) {
      // 调用postpatch钩子函数
      i(oldVnode, vnode);
    }
  }
}

代码解释:

这段代码是用来更新虚拟DOM节点的,它会对比新旧节点的属性和子节点,并进行相应的更新操作

  1. 首先,它检查新旧节点的引用是否相同,如果相同,则直接返回,无需进行任何更新操作。
  2. 如果新节点具有elm属性并且ownerArray也被定义,说明节点是被复用的,它会克隆新节点。
  3. 将新节点的elm属性设置为旧节点的elm属性,以保持引用的一致性。
  4. 如果旧节点是异步占位符节点(例如v-if指令的节点),则根据新节点是否已经解析,决定是执行hydrate操作(将新节点添加到旧节点中)还是将新节点标记为异步占位符。
  5. 如果新节点和旧节点都是静态节点,并且它们具有相同的key,并且满足以下条件之一:新节点被克隆或者具有v-once指令,那么只需将旧节点的组件实例赋值给新节点的组件实例,并返回。
  6. 如果新节点具有data属性,并且该节点可进行补丁操作(isPatchable函数返回true),则遍历调用cbs.update数组中的各个函数来更新旧节点的属性。如果data.hook.update钩子函数被定义,则也会调用它。
  7. 如果新节点不是文本节点(即具有子节点):
    • 如果旧节点和新节点都具有子节点,并且它们不相等,则调用updateChildren函数来对比和更新子节点。
    • 如果只有新节点具有子节点,而旧节点没有子节点,则清空旧节点的文本内容,并通过addVnodes函数添加新的子节点。
    • 如果只有旧节点具有子节点,而新节点没有子节点,则通过removeVnodes函数删除旧节点的子节点。
    • 如果旧节点是文本节点,则清空旧节点的文本内容。
  1. 如果新节点是文本节点,并且新节点的文本内容与旧节点的文本内容不同,则更新旧节点的文本内容为新节点的文本内容。
  2. 如果新节点的data属性被定义,且存在data.hook.postpatch钩子函数,则调用它

核心部分updateChildren()

手动实现vue2的updateChildren:

实现思路

我们首先先定义四个指针(下标)指向两个列表的头尾

function vue2Diff(prevChildren, nextChildren, parent) {
  let oldStartIndex = 0,
    oldEndIndex = prevChildren.length - 1
  newStartIndex = 0,
    newEndIndex = nextChildren.length - 1;
  let oldStartNode = prevChildren[oldStartIndex],
    oldEndNode = prevChildren[oldEndIndex],
    newStartNode = nextChildren[nextStartIndex],
    newEndNode = nextChildren[nextEndIndex];
}

根据四个指针找到四个节点,然后进行对比,那么如何对比呢?我们按照以下四个步骤进行对比

概括:头头对比/尾尾对比/头尾对比/尾头对比

  1. 使用旧列表的头⼀个节点 oldStartNode 与新列表的头⼀个节点 newStartNode 对比;
  2. 使用旧列表的最后⼀个节点 oldEndNode 与新列表的最后⼀个节点 newEndNode 对比;
  3. 使用旧列表的头⼀个节点 oldStartNode 与新列表的最后⼀个节点 newEndNode 对比;
  4. 使用旧列表的最后⼀个节点 oldEndNode 与新列表的头⼀个节点 newStartNode 对比;

使用以上四步进行对比,去寻找key相同的可复用的节点,当在某⼀步中找到了则停止后面的寻找。具

体对比顺序如下图:

对比顺序代码结构如下:

function vue2Diff(prevChildren, nextChildren, parent) {
  let oldStartIndex = 0,
    oldEndIndex = prevChildren.length - 1
  newStartIndex = 0,
    newEndIndex = nextChildren.length - 1;
  let oldStartNode = prevChildren[oldStartIndex],
    oldEndNode = prevChildren[oldEndIndex],
    newStartNode = nextChildren[nextStartIndex],
    newEndNode = nextChildren[nextEndIndex];

  if (oldStartNode.key === newStartNode.key) {

  } else if (oldEndNode.key === newEndNode.key) {

  } else if (oldStartNode.key === newEndNode.key) {

  } else if (oldEndNode.key === newStartNode.key) {

  }
}

如以上代码当对比时找到了可复用的节点,我们还是先 patch 给元素打补丁,然后将指针进行前/后移⼀位指针。根据对比节点的不同,我们移动的指针和方向也不同,具体规则如下:

1. 当旧列表的头⼀个节点 oldStartNode 与新列表的头⼀个节点 newStartNode 对比时key相

同。那么旧列表的头指针 oldStartIndex 与新列表的头指针 newStartIndex 同时向后移动⼀位

2. 当旧列表的最后⼀个节点 oldEndNode 与新列表的最后⼀个节点 newEndNode 对比时key相

同。那么旧列表的尾指针oldEndIndex与新列表的尾指针 newEndIndex 同时向前移动⼀位;

3. 当旧列表的头⼀个节点 oldStartNode 与新列表的最后⼀个节点 newEndNode 对比时key相

同。那么旧列表的头指针 oldStartIndex 向后移动⼀位;新列表的尾指针 newEndIndex 向

前移动⼀位;

4. 当旧列表的最后⼀个节点 oldEndNode 与新列表的头⼀个节点 newStartNode 对比时key相

同。那么旧列表的尾指针 oldEndIndex 向前移动⼀位;新列表的头指针 newStartIndex 向

后移动⼀位;

编写成代码如下可视:

function vue2Diff(prevChildren, nextChildren, parent) {
  let oldStartIndex = 0,
    oldEndIndex = prevChildren.length - 1
  newStartIndex = 0,
    newEndIndex = nextChildren.length - 1;
  let oldStartNode = prevChildren[oldStartIndex],
    oldEndNode = prevChildren[oldEndIndex],
    newStartNode = nextChildren[nextStartIndex],
    newEndNode = nextChildren[nextEndIndex];

  if (oldStartNode.key === newStartNode.key) {
    patch(oldvStartNode, newStartNode, parent)
    oldStartIndex++
    newStartIndex++
    oldStartNode = prevChildren[oldStartIndex]
    newStartNode = nextChildren[newStartIndex]
  } else if (oldEndNode.key === newEndNode.key) {
    patch(oldEndNode, newEndNode, parent)
    oldEndIndex--
    newEndIndex--
    oldEndNode = prevChildren[oldEndIndex]
    newEndNode = nextChildren[newEndIndex]
  } else if (oldStartNode.key === newEndNode.key) {
    patch(oldStartNode, newEndNode, parent)
    oldStartIndex++
    newEndIndex--
    oldStartNode = prevChildren[oldStartIndex]
    newEndNode = nextChildren[newEndIndex]
  } else if (oldEndNode.key === newStartNode.key) {
    patch(oldEndNode, newStartNode, parent)
    oldEndIndex--
    nextStartIndex++
    oldEndNode = prevChildren[oldEndIndex]
    newStartNode = nextChildren[newStartIndex]
  }
}

上面提到,要让指针向内靠拢,所以我们需要循环。循环停止的条件是当其中⼀个列表的节点全部遍

历完成(即老的头下标小于等于老的尾下标并且新的头下标小于新的尾下标),代码如下:

function vue2Diff(prevChildren, nextChildren, parent) {
  let oldStartIndex = 0,
    oldEndIndex = prevChildren.length - 1
  newStartIndex = 0,
    newEndIndex = nextChildren.length - 1;
  let oldStartNode = prevChildren[oldStartIndex],
    oldEndNode = prevChildren[oldEndIndex],
    newStartNode = nextChildren[nextStartIndex],
    newEndNode = nextChildren[nextEndIndex];

  while (oldStartIndex <= oldEndIndex && newStartIndex <= newEndIndex) {
    if (oldStartNode.key === newStartNode.key) {
      patch(oldvStartNode, newStartNode, parent)
      oldStartIndex++
      newStartIndex++
      oldStartNode = prevChildren[oldStartIndex]
      newStartNode = nextChildren[newStartIndex]
    } else if (oldEndNode.key === newEndNode.key) {
      patch(oldEndNode, newEndNode, parent)
      oldEndIndex--
      newEndIndex--
      oldEndNode = prevChildren[oldEndIndex]
      newEndNode = nextChildren[newEndIndex]
    } else if (oldStartNode.key === newEndNode.key) {
      patch(oldStartNode, newEndNode, parent)
      oldStartIndex++
      newEndIndex--
      oldStartNode = prevChildren[oldStartIndex]
      newEndNode = nextChildren[newEndIndex]
    } else if (oldEndNode.key === newStartNode.key) {
      patch(oldEndNode, newStartNode, parent)
      oldEndIndex--
      nextStartIndex++
      oldEndNode = prevChildren[oldEndIndex]
      newStartNode = nextChildren[newStartIndex]
    }
  }

}

至此整体的循环我们就全部完成了,下⾯我们需要考虑这样两个问题:

• 什么情况下DOM节点需要移动;

• DOM节点如何移动;

什么情况下DOM节点需要移动

我们以上面的图为例子

理想状态的情况

当我们在第⼀个循环时,在第四步发现旧列表的尾节点 oldEndNode 与新列表的头节点 newStartNode 的key相同,是可复用的DOM节点。通过观察我们可以发现,原本在旧列表末尾的节点,却是新列表中的开头节点,没有人比他更靠前,因为他是第⼀个,所以我们只需要把当前的节 点移动到原本旧列表中的第⼀个节点之前,让它成为第一个节点即可。代码如下

function vue2Diff(prevChildren, nextChildren, parent) {
  // ...
  while (oldStartIndex <= oldEndIndex && newStartIndex <= newEndIndex) {
    if (oldStartNode.key === newStartNode.key) {
      // ...
    } else if (oldEndNode.key === newEndNode.key) {
      // ...
    } else if (oldStartNode.key === newEndNode.key) {
      // ...
    } else if (oldEndNode.key === newStartNode.key) {
      patch(oldEndNode, newStartNode, parent)
      // 移动到旧列表头节点之前
      parent.insertBefore(oldEndNode.el, oldStartNode.el)

      oldEndIndex--
      newStartIndex++
      oldEndNode = prevChildren[oldEndIndex]
      newStartNode = nextChildren[newStartIndex]
    }
  }
}
}

进⼊第⼆次循环,我们在第⼆步发现,旧列表的尾节点 oldEndNode 和新列表的尾节点 newEndNode 为复用节点。原本在旧列表中就是尾节点,在新列表中也是尾节点,说明该节点不需要移动,所以我们什么都不需要做。

同理,如果是旧列表的头节点 oldStartNode 和新列表的头节点 newStartNode 为复用节点,我

们也什么都不需要做

进⼊第三次循环,我们在第三部发现,旧列表的头节点 oldStartNode 和新列表的尾节点

newEndNode 为复⽤节点。,我们只要将DOM-A移动到DOM-B后⾯就可以了。

依照惯例我们还是解释⼀下,原本旧列表中是头节点,然后在新列表中是尾节点。那么只要在旧列表

中把当前的节点移动到原本尾节点的后⾯,就可以了。

代码如下:

function vue2Diff(prevChildren, nextChildren, parent) {
  // ...
  while (oldStartIndex <= oldEndIndex && newStartIndex <= newEndIndex) {
    if (oldStartNode.key === newStartNode.key) {
      // ...
    } else if (oldEndNode.key === newEndNode.key) {
      // ...
    } else if (oldStartNode.key === newEndNode.key) {
      patch(oldStartNode, newEndNode, parent)
      parent.insertBefore(oldStartNode.el, oldEndNode.el.nextSibling)

      oldStartIndex++
      newEndIndex--
      oldStartNode = prevChildren[oldStartIndex]
      newEndNode = nextChildren[newEndIndex]
    } else if (oldEndNode.key === newStartNode.key) {
      //...
    }
  }
}

进⼊最后⼀个循环。在第⼀步旧列表头节点 oldStartNode 与新列表头节点 newStartNode 位置

相同,所以啥也不⽤做。然后结束循环。

非理想情况

上⽂中有⼀个特殊情况,当四次对比都没找到复用节点时,我们只能拿新列表的第⼀个节点去旧列表

中找与其key相同的节点。如下图所示

function vue2Diff(prevChildren, nextChildren, parent) {
  //...
  while (oldStartIndex <= oldEndIndex && newStartIndex <= newEndIndex) {
    if (oldStartNode.key === newStartNode.key) {
      //...
    } else if (oldEndNode.key === newEndNode.key) {
      //...
    } else if (oldStartNode.key === newEndNode.key) {
      //...
    } else if (oldEndNode.key === newStartNode.key) {
      //...
    } else {
      // 在旧列表中找到 和新列表头节点key 相同的节点
      let newKey = newStartNode.key,
        oldIndex = prevChildren.findIndex(child => child.key === newKey);

    }
  }
}

拿新列表的节点去旧列表中找只有两种情况:

  • 一种是在旧列表找到了节点--此时应该移动节点
  • 一种在就列表找不到对应的节点--此时应该新增节点

第一种情况如图所示

当我们在旧列表中找到对应的VNode,我们只需要将找到的节点的DOM元素,移动到开头就可以了。

这里的逻辑其实和第四步的逻辑是⼀样的,只不过第四步是移动的尾节点,这里是移动找到的节点。

DOM移动后,由我们将旧列表中的节点改为undefined,这是至关重要的⼀步,因为我们已经做了节

点的移动了所以我们不需要进行再次的对比了。最后我们将头指针newStartIndex向后移⼀位

代码如下

function vue2Diff(prevChildren, nextChildren, parent) {
  //...
  while (oldStartIndex <= oldEndIndex && newStartIndex <= newEndIndex) {
    if (oldStartNode.key === newStartNode.key) {
      //...
    } else if (oldEndNode.key === newEndNode.key) {
      //...
    } else if (oldStartNode.key === newEndNode.key) {
      //...
    } else if (oldEndNode.key === newStartNode.key) {
      //...
    } else {
      // 在旧列表中找到 和新列表头节点key 相同的节点
      let newtKey = newStartNode.key,
        oldIndex = prevChildren.findIndex(child => child.key === newKey);

      if (oldIndex > -1) {
        let oldNode = prevChildren[oldIndex];
        patch(oldNode, newStartNode, parent)
        parent.insertBefore(oldNode.el, oldStartNode.el)
        prevChildren[oldIndex] = undefined
      }
      newStartNode = nextChildren[++newStartIndex]
    }
  }
}

第二种情况如果在旧列表中没有找到复⽤节点,就直接创建⼀个新的节点放到最前⾯就可以了,然后后移头指针 newStartIndex 。如图所示:

function vue2Diff(prevChildren, nextChildren, parent) {
  //...
  while (oldStartIndex <= oldEndIndex && newStartIndex <= newEndIndex) {
    if (oldStartNode.key === newStartNode.key) {
      //...
    } else if (oldEndNode.key === newEndNode.key) {
      //...
    } else if (oldStartNode.key === newEndNode.key) {
      //...
    } else if (oldEndNode.key === newStartNode.key) {
      //...
    } else {
      // 在旧列表中找到 和新列表头节点key 相同的节点
      let newtKey = newStartNode.key,
        oldIndex = prevChildren.findIndex(child => child.key === newKey);

      if (oldIndex > -1) {
        let oldNode = prevChildren[oldIndex];
        patch(oldNode, newStartNode, parent)
        parent.insertBefore(oldNode.el, oldStartNode.el)
        prevChildren[oldIndex] = undefined
      } else {
        mount(newStartNode, parent, oldStartNode.el)
      }
      newStartNode = nextChildren[++newStartIndex]
    }
  }
}

我们上面说到若是找到相同的把节点改为undefined的,此时我们遍历需要考虑这种情况,所以当旧列表遍历到undefind时就跳过当前节点。代码如下:

function vue2Diff(prevChildren, nextChildren, parent) {
  //...
  while (oldStartIndex <= oldEndIndex && newStartIndex <= newEndIndex) {
    //旧节点才比较
    //若是头部找到undefine则往后移动一位
    if (oldStartNode === undefind) {
      oldStartNode = prevChildren[++oldStartIndex]
    } else if (oldEndNode === undefind) {
       //若是尾部找到undefine则往前移动一位
      oldEndNode = prevChildren[--oldEndIndex]
    } else if (oldStartNode.key === newStartNode.key) {
      //...
    } else if (oldEndNode.key === newEndNode.key) {
      //...
    } else if (oldStartNode.key === newEndNode.key) {
      //...
    } else if (oldEndNode.key === newStartNode.key) {
      //...
    } else {
      // ...
    }
  }
}

添加节点的情况

如图所示:

此时 oldEndIndex 以及小于了 oldStartIndex ,但是新列表中还有剩余的节点,我们只需要将剩余的节点依次插入到 oldStartNode 的DOM之前就可以了。

为什么是插入 oldStartNode 之前呢?原因是剩余的节点在新列表的位置是位于 oldStartNode 之前的,如果剩余节点是在 oldStartNode 之后,oldStartNode 就会先行对比

function vue2Diff(prevChildren, nextChildren, parent) {
  //...
  while (oldStartIndex <= oldEndIndex && newStartIndex <= newEndIndex) {
    // ...
  }
  if (oldEndIndex < oldStartIndex) {
    for (let i = newStartIndex; i <= newEndIndex; i++) {
      mount(nextChildren[i], parent, prevStartNode.el)
    }
  }
}

移除节点的情况

当新列表的 newEndIndex 小于 newStartIndex 时,我们将旧列表剩余的节点删除即可。这里我

们需要注意,旧列表的undefind。在第二小节中我们提到过,当头尾节点都不相同时,我们会去旧列

表中找新列表的第⼀个节点,移动完DOM节点后,将旧列表的那个节点改为undefind。所以我们在最

后的删除时,需要注意这些undefind,遇到的话跳过当前循环即可。

function vue2Diff(prevChildren, nextChildren, parent) {
  //...
  while (oldStartIndex <= oldEndIndex && newStartIndex <= newEndIndex) {
    // ...
  }
  if (oldEndIndex < oldStartIndex) {
    for (let i = newStartIndex; i <= newEndIndex; i++) {
      mount(nextChildren[i], parent, prevStartNode.el)
    }
  } else if (newEndIndex < newStartIndex) {
    for (let i = oldStartIndex; i <= oldEndIndex; i++) {
      if (prevChildren[i]) {
        partent.removeChild(prevChildren[i].el)
      }
    }
  }
}

总结

最终手写实现updateChildren代码如下:

function vue2diff(prevChildren, nextChildren, parent) {
  let oldStartIndex = 0,
    newStartIndex = 0,
    oldStartIndex = prevChildren.length - 1,
    newStartIndex = nextChildren.length - 1,
    oldStartNode = prevChildren[oldStartIndex],
    oldEndNode = prevChildren[oldStartIndex],
    newStartNode = nextChildren[newStartIndex],
    newEndNode = nextChildren[newStartIndex];
  while (oldStartIndex <= oldStartIndex && newStartIndex <= newStartIndex) {
    if (oldStartNode === undefined) {
      oldStartNode = prevChildren[++oldStartIndex]
    } else if (oldEndNode === undefined) {
      oldEndNode = prevChildren[--oldStartIndex]
    } else if (oldStartNode.key === newStartNode.key) {
      patch(oldStartNode, newStartNode, parent)

      oldStartIndex++
      newStartIndex++
      oldStartNode = prevChildren[oldStartIndex]
      newStartNode = nextChildren[newStartIndex]
    } else if (oldEndNode.key === newEndNode.key) {
      patch(oldEndNode, newEndNode, parent)

      oldStartIndex--
      newStartIndex--
      oldEndNode = prevChildren[oldStartIndex]
      newEndNode = nextChildren[newStartIndex]
    } else if (oldStartNode.key === newEndNode.key) {
      patch(oldStartNode, newEndNode, parent)
      parent.insertBefore(oldStartNode.el, oldEndNode.el.nextSibling)
      oldStartIndex++
      newStartIndex--
      oldStartNode = prevChildren[oldStartIndex]
      newEndNode = nextChildren[newStartIndex]
    } else if (oldEndNode.key === newStartNode.key) {
      patch(oldEndNode, newStartNode, parent)
      parent.insertBefore(oldEndNode.el, oldStartNode.el)
      oldStartIndex--
      newStartIndex++
      oldEndNode = prevChildren[oldStartIndex]
      newStartNode = nextChildren[newStartIndex]
    } else {
      let newKey = newStartNode.key,
        oldIndex = prevChildren.findIndex(child => child && (child.key === newKe
                                                             if (oldIndex === -1) {
        mount(newStartNode, parent, oldStartNode.el)
      } else {
        let prevNode = prevChildren[oldIndex]
        patch(prevNode, newStartNode, parent)
        parent.insertBefore(prevNode.el, oldStartNode.el)
        prevChildren[oldIndex] = undefined
      }
      newStartIndex++
      newStartNode = nextChildren[newStartIndex]
    }
  }
  if (newStartIndex > newStartIndex) {
    while (oldStartIndex <= oldStartIndex) {
      if (!prevChildren[oldStartIndex]) {
        oldStartIndex++
        continue
      }
      parent.removeChild(prevChildren[oldStartIndex++].el)
    }
  } else if (oldStartIndex > oldStartIndex) {
    while (newStartIndex <= newStartIndex) {
      mount(nextChildren[newStartIndex++], parent, oldStartNode.el)
    }
  }
}

缺点

Vue2 是全量 Diff(当数据发生变化,它就会新生成⼀个DOM树,并和之前的DOM树进行比较,找到不

同的节点然后更新),如果层级很深,很消耗内存;所以Vue3因此做出的改变

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值