站在巨人的肩膀上看vue3-第11章 快速Diff算法

站在巨人的肩膀上看vue,来自霍春阳的vue设计与实现。作者以问题的形式一步步解读vue3底层的实现。思路非常的巧妙,这里会将书中的主要逻辑进行串联,也是自己读后的记录,希望通过这种形式可以和大家一起交流学习。

开篇

快速算法运用在vue3中,借鉴了文本Diff中的预处理,先处理新旧节点相同位置的前置节点和后置节点。当前置节点后后置节点全部处理完之后出现三种情况:

相同的前后置节点处理完之后之后,旧的节点全部处理了,新的节点还有,这时候就需要新增,遍历剩下的新节点插入到下一个节点前面。

同理,相同的前后置节点处理完之后之后,新的节点都全部处理了,旧的节点还有,这时候就需要卸载,遍历旧的节点卸载。

当两组中还有未处理的节点情况,这时候就需要找到哪些是需要新增,哪些是需要卸载的。通过简单Diff算法那一章节讲到,遍历旧节点找到最大索引值,如果新节点的key没有呈递增规律,则表示是需要移动的节点,

这时候可以构建一个数组sourcer存储剩于新节点在旧节点的位置,如果值为-1那表明这是一个新增的节点。

然后找到sourcer的最大递增子序列,从尾开始遍历,当尾部节点和source的值不相等,则表示需要移动,调用patch移动,否则只需要让s指向下一个位置,直到全部遍历完。

这就是快速Diff算法的过程。

11.1、相同的前置元素和后置元素

新旧两组节点在中的位置不变,无需移动他们。while循环查找所有相同的前后置节点,并调用patch函数更新,直到key值不同的节点位置。

function patchKeyedChildren(newChildren, oldChildren, container) {
  // 相同的前置节点
  let j = 0
  let oldVNode = oldChildren[j]
  let newVNode = newChildren[j]
  while (oldVNode.key === newVNode.key) {
    patch(oldVNode, newVNode, container)
    j++
    oldVNode = oldChildren[j]
    newVNode = newChildren[j]
  }
  // 相同的后置节点
  let newEnd = newChildren.length - 1
  let oldEnd = oldChildren.length - 1
  let newEndVNode = newChildren[newEnd]
  let oldEndVNode = oldChildren[oldEnd]
  while (newEndVNode.key === oldEndVNode.key) {
    patch(oldEndVNode, newEndVNode, container)
    oldEnd--
    newEnd--
    oldEndVNode = oldChildren[oldEnd]
    newEndVNode = newChildren[newEnd]
  }
}
复制代码

情况1:相同的前后置节点处理完之后之后,旧的节点都全部处理了,新的节点还有(新增)

满足的条件

扫描二维码关注公众号,回复: 13754471 查看本文章
  1. oldEnd < j 成立。 旧节点走处理完毕
  2. endEnd >= j 成立,新节点还有未处理,需要新增

新增的时候计算锚点索引值anchorIndex = newEnd + 1,如果小于新的一组节点的数量,说明锚点元素在新的节点中,相反对应的是节点的尾部,这时不需要锚点

if (j > oldEnd && j <= newEnd) {
  const anchorIndex = newEnd + 1
  const anchor = anchorIndex < newChildren.length ? newChildren[anchorIndex].el : null
  while (j <= newEnd) {
    patch(null, newChildren[j++], container, anchor)
  }
}
复制代码

情况2:相同的前后置节点处理完之后之后,新的节点都完全处理了,旧的节点还有(删除)

满足的条件

  1. j ≤ oldEnd 成立。 旧节点走还有,需要删除
  2. j > newEnd 成立 新节点处理完毕
if (j > oldEnd && j <= newEnd) { // 新增
    const anchorIndex = newEnd + 1
    const anchor = anchorIndex < newChildren.length ? newChildren[anchorIndex].el : null
    while (j <= newEnd) {
      patch(null, newChildren[j++], container, anchor)
    }
  } else if (j > newEnd && j <= oldEnd) { // 删除
    while (j <= oldEnd) {
      unmount(oldChildren[j++])
    }
  }
复制代码

11.2、判断是否需要进行DOM移动操作

之前都是两组节点终会有一组节点全部被处理,这种情况只需要简单挂载、卸载节点即可。

情况3:在新旧节点种有新增和需要删除的情况

  1. 构造一个数组source,长度等于新节点预处理过之后的节点的数量,初始值都是 -1,用来存储新节点在旧节点中的位置索引
  2. 为新节点构建一张索引表keyIndex,存储节点的key和位置索引之间的映射
  3. 遍历旧的节点,旧节点的k去索引表中查找新节点的位置,存储在k中
  4. 如果有k说用该节点可以复用,调用patch更新,填充source数组,否则需要卸载
let newStart = j
let oldStart = j
const count = newEnd - j + 1
const source = new Array(count).fill(-1)
const keyIndex = {} // 索引表
for (let i = newStart; i <= newEnd; i++) {
  keyIndex[newChildren[i].key] = i
}
for (let i = oldStart; i <= oldEnd; i++) {
  oldVNode = oldChildren[i]
  const k = keyIndex[oldVNode.key]
  if (typeof k !== 'undefined') {
    let newVNode = newChildren[k]
    patch(oldVNode, newVNode, container)
    source[k - newStart] = i
  } else {
    unmount(oldVNode)
  }
}
复制代码
  1. 如何判断节点需要移动

遍历旧节点过程遇到的索引值呈现递增趋势,说明不需要移动节点,反之需要。定义一个pos,当k< pos 则需要移动,相反不需要移动,更新pos值

let moved = false
let pos = 0
for (let i = oldStart; i <= oldEnd; i++) {
  oldVNode = oldChildren[i]
  const k = keyIndex[oldVNode.key]
  if (typeof k !== 'undefined') {
    let newVNode = newChildren[k]
    patch(oldVNode, newVNode, container)
    source[k - newStart] = i
    if (k < pos) {  // 判断节点需要移动
      moved = true
    } else {
      pos = k
    }
  } else {
    unmount(oldVNode)
  }
}
复制代码
  1. 新增一个数量标识,代表已经更新过的节点数量,如果更新过的节点数量比新节点需要更新的数量多时,说明有多余节点需要删除

11.3、如何移动元素

寻找source最长递增子系列,找到不需要移动的节点。const seq = getSequence(sources)

seq 存储的是source的索引。

seq  source    KeyIndex     新节点     旧节点
                            
0      2          3:1         p-3       p-2
1 (s)  3          4:2         p-4       p-3
       1          2:3         p-2       p-4
      -1          7:4         p-7  (i)  p-6
                             
复制代码

seq表示在新节点中索引值不需要移动,3跟4节点不需要移动,只需要移动2和7

创建两个变量i和s,遍历新节点,如果seq[s] != i 则说明需要移动,否则只需要让s指向一下一个位置

如果source[i] === -1 直接新增,挂载在新节点下一个节点

const seq = getSequence(source)
let s = seq.length - 1
let i = count - 1
for (i; i >= 0; i--) {
  if (source[i] === -1) {
    const pos = i + newStart
    const newVNode = newChildren[pos]
    const nextPos = pos + 1
    const anchor = nextPos < newChildren.length ? newChildren[nextPos].el : null
    insert(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
    patch(newVNode.el, container, anchor)
  } else {
    s--
  }
}
复制代码

猜你喜欢

转载自juejin.im/post/7080449431014735908