Vue设计与实现 笔记 Diff算法

101 阅读3分钟

简单Diff算法

之前没有Diff算法时候的写法是卸载所以旧节点挂载所有新节点,那样的化DOM操作太多。 在简单Diff算法中

  • 通过两层for循环,遍历新旧子节点,通过key来将新旧节点对应起来,然后进行移动
  • 如果遍历过程中,新节点中有的key在旧节点中不存在,那就执行挂载操作;
  • 如果有旧节点的key在新节点中不存在,则执行卸载操作

如何判断移动? 拿新的一组子节点中的节点去旧的一组子节点中寻找可复用的节点,如果找到了,记录该节点的位置索引,称为最大索引,在整个更新过程中,如果有节点的索引小于最大索引,那么说明这个节点对应的真实DOM需要移动。

双端Diff算法(Vue2)

每一轮都进行下面这样的对比操作,能找到可复用的节点则进行patch和移动等操作

image.png **非理想情况:**在一轮比较中,四次都无法命中可复用节点,则可在oldVNode中寻找newStartIdx的可复用节点,然后进行patch和移动,并把oldVNode中,与newStartIdx对应的节点设为undefined(因为该节点已经移动到别处了)

image.png

image.png

**添加节点:**某个新节点即使在oldVNode中遍历也找不到,那就是新的节点,循环结束后,遍历newStartIdx和newEndIdx之间的节点,就可以完成对新增元素的处理。

**移除不存在的元素:**与新增类似,遍历oldStartIdx和oldEndIdx之间的节点,即可逐一卸载。

快速Diff算法(Vue3)

快速Diff借鉴了纯文本Diff算法,

**source数组:**该数组的长度为新的一组子节点去掉相同的前置、后置节点之后,未处理的节点的长度。用来储存一组子节中的节点在旧的一组子节点中的位置索引,后面将会使用它计算一个最长递增子序列,并用于辅助完成DOM移动的操作。

image.png

但是两次循环来构建source数组的时间复杂度(O(n1*n2))还是高,所以使用索引表来填充source数组

**索引表:**用来存储节点的key和节点位置索引之间的映射。

image.png

快速Diff算法判断节点是否需要移动的方法和简单Diff算法类似

创建moved作为标识,它的值为true时,说明需要DOM移动操作,构建source数组,用于DOM移动操作。

source数组的最长递增子序列用seq表示,并用变量s指向seq的最后一个位置,变量i指向source的最后一个位置。

image.png

然后,更新过程分为三个步骤:

  • 判断source[i]的值是否为-1,如果是-1,则为全新节点,进行挂载,如果不是,进行下一步
  • 判断i !== seq[s],if true,则该节点需要移动,else,第三步
  • 到了第三步,该节点不需要移动,但如果s不为0,则需要进行s--操作,指向下一个位置
if(moved) {
  const seq = lis(sources)
  // s指向最长递增子序列的最后一个元素
  let s = seq.length - 1
  let i = count - 1
  for(i; i >= 0; i--) {
    if(source[i] === -1) {
      // 是全新节点,需要挂载
      // 该节点在新 children 中的真实位置索引
      const pos = i + newStart
      const newVNode = newChildren[pos]
      // 该节点的下一个节点的位置索引
      const nextPos = pos + 1
      // 锚点
      const anchor = nextPos < newChildren.length ? newChildren[nextPos].el : null
      // 挂载
      patch(null, newVNode, container, anchor)
    } else if(i !== seq[s]) {
      // 说明节点需要移动
      // 该节点在新的一组子节点中的真实位置索引
      const pos = i + newStart
      const newVNode = newChildren[pos]
      // 该节点的下一个节点的位置索引
      const nextPos = pos + 1
      // 锚点
      const anchor = nextPos < newChildren.length ? newChildren[nextPos].el : null
      // 移动
      insert(newVNode.el, container, anchor)
    } else {
      // 当 i === seq[s]时,说明该位置的节点不需要移动
      // 并让s指向下一个位置
      s--
    } 
  }
}

最长的递增子序列

获取给定序列最长的递增子序列的方法:Vue3采用的是贪心 + 二分查找:O(nlogn)

function getSequence(arr) {
    /* p 的作用:回溯:使用前驱索引纠正最长递增子序列的偏差
     数组每一项保存应该排在当前元素前面元素的下标。
     然后经过逆序遍历数组 p,纠正 result 数组的元素 */
    const p = arr.slice()
    const result = [0]
    let i, j, u, v, c
    const len = arr.length
    for (i = 0; i < len; i++) {
        const arrI = arr[i]
        if (arrI !== 0) {
            j = result[result.length - 1]
            if (arrI > arr[j]) {
                // 如果数组当前的数比result里面索引的最后一项大
                // 则把当前索引也存入result
                result.push(i)
                p[i] = j
                continue
            }
            // 要开始二分了
            /*
            二分找到某一项刚好大于当前项,此时 u 和 v 指针应
            该是指向同一个元素下标,然后用当前元素替换掉二分找到的那一项。
            */
            u = 0  // 左
            v = result.length - 1  // 右
            while (u < v) {
                c = ((u + v) / 2) | 0 // 中
                // 虽然arr是无序的,但是arr[result[?]]是有序的
                if (arr[result[c]] < arrI) {
                    u = c + 1
                } else {
                    v = c
                }
            }
            // while 循环结束后,u 和 v 会指向同一个元素
            if (arrI < arr[result[u]]) {
                if (u > 0) {
                    p[i] = result[u - 1]
                }
                result[u] = i
            }
        }
    }
    u = result.length
    v = result[u - 1]
    while (u-- > 0) {
        result[u] = v
        v = p[v]
    }
    return result
}

let nums = [3, 2, 8, 9, 5, 6, 7, 11, 15, 4]
console.log(getSequence(nums).map(e => nums[e]))