[Day08 vue中diff算法 | 青训营笔记]

14 阅读3分钟

这是我参与「第五届青训营 」笔记创作活动的第8天.

一、详细知识点介绍:

VUE中的diff算法

diff的由来

Vue 和 React 都是基于 vdom 的前端框架,组件渲染会返回 vdom,渲染器再把 vdom 通过增删改的 api 同步到 dom。

当再次渲染时,会产生新的 vdom,渲染器会对比两棵 vdom 树,对有差异的部分通过增删改的 api 更新到 dom。

这里对比两棵 vdom 树,找到有差异的部分的算法,就叫做 diff 算法。

diff算法

两棵树作对比,因为每个节点都要进行比较,很显然时间复杂度是O(n^3),这样的时间复杂度其实就有点不可接受了,因为大家都知道次方的积数就很大了。

所以前端框架就做了一个规定,只做同层的对比,type相同就不对比子节点了。

很显然,上图的时间复杂度就变成了O(n)。这样1000个节点只做1000次对比,相对于上面那个就小很多了。

但是这样的话也存在着一些问题,假如ABCDE变成了BCDEA,逐一对比发现这五个节点不同然后就会渲染。但其实你会发现只要所有节点跟着移动一下就好了。

简单diff算法

多节点 diff 算法的目的是为了尽量复用节点,通过移动节点代替创建。

所以新 vnode 数组的每个节点我们都要找下在旧 vnode 数组中有没有对应 key 的,有的话就移动到新的位置,没有的话再创建新的。

小结一下:

diff 算法的目的是根据 key 复用 dom 节点,通过移动节点而不是创建新节点来减少 dom 操作。

对于每个新的 vnode,在旧的 vnode 中根据 key 查找一下,如果没查找到,那就新增 dom 节点,如果查找到了,那就可以复用。

复用的话要不要移动要判断下下标,如果下标在 lastIndex 之后,就不需要移动,因为本来就在后面,反之就需要移动。

最后,把旧的 vnode 中在新 vnode 中没有的节点从 dom 树中删除。

这个 diff 算法我们是从一端逐个处理的,叫做简单 diff 算法。

简单 diff 算法其实性能不是最好的,比如旧的 vnode 数组是 ABCD,新的 vnode 数组是 DABC,按照简单 diff 算法,A、B、C 都需要移动。

那怎么优化这个算法呢?

从一个方向顺序处理会有这个问题,那从两个方向同时对比呢?

这就是双端 diff 算法:

双端diff算法

简单 diff 算法能够实现 dom 节点的复用,但有的时候会做一些没必要的移动。双端 diff 算法解决了这个问题,它是从两端进行对比。

头和尾的指针向中间移动,直到 oldStartIdx <= oldEndIdx 并且 newStartIdx <= newEndIdx,说明就处理完了全部的节点。

每次对比下两个头指针指向的节点、两个尾指针指向的节点,头和尾指向的节点,是不是 key是一样的,也就是可复用的。

如果是可复用的话就直接用,调用 patch 更新一下,如果是头尾这种,还要移动下位置。

while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
  if (oldStartVNode.key === newStartVNode.key) { // 头头
    patch(oldStartVNode, newStartVNode, container)
    oldStartVNode = oldChildren[++oldStartIdx]
    newStartVNode = newChildren[++newStartIdx]
  } else if (oldEndVNode.key === newEndVNode.key) {//尾尾
    patch(oldEndVNode, newEndVNode, container)
    oldEndVNode = oldChildren[--oldEndIdx]
    newEndVNode = newChildren[--newEndIdx]
  } else if (oldStartVNode.key === newEndVNode.key) {//头尾,需要移动
    patch(oldStartVNode, newEndVNode, container)
    insert(oldStartVNode.el, container, oldEndVNode.el.nextSibling)

    oldStartVNode = oldChildren[++oldStartIdx]
    newEndVNode = newChildren[--newEndIdx]
  } else if (oldEndVNode.key === newStartVNode.key) {//尾头,需要移动
    patch(oldEndVNode, newStartVNode, container)
    insert(oldEndVNode.el, container, oldStartVNode.el)

    oldEndVNode = oldChildren[--oldEndIdx]
    newStartVNode = newChildren[++newStartIdx]
  } else {
    
    // 头尾没有找到可复用的节点
  }
}

小结一下:

双端 diff 是头尾指针向中间移动的同时,对比头头、尾尾、头尾、尾头是否可以复用,如果可以的话就移动对应的 dom 节点。

如果头尾没找到可复用节点就遍历 vnode 数组来查找,然后移动对应下标的节点到头部。

最后还剩下旧的 vnode 就批量删除,剩下新的 vnode 就批量新增。

二、引用参考: