《Vue.js 设计与实现》读书笔记 - 第11章、快速 Diff 算法

第11章、快速 Diff 算法

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

快速 Diff 算法包含预处理步骤,这借鉴了纯文本 Diff 算法的思路。

先把相同的前缀后缀进行处理,然后再比较中间部分。

function patchKeyedChildren(n1, n2, container) {
  const oldChildren = n1.children
  const newChildren = n2.children
  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 oldEnd = oldChildren.length - 1
  let newEnd = newChildren.length - 1
  oldVNode = oldChildren[oldEnd]
  newVNode = newChildren[newEnd]
  while (oldVNode.key === newVNode.key) {
    patch(oldVNode, newVNode, container)
    oldEnd--
    newEnd--
    oldVNode = oldChildren[oldEnd]
    newVNode = newChildren[newEnd]
  }
}

如果旧节点都被处理完了,新节点还有剩余,证明这些都是新增的,需要依次挂载。而如果是旧节点有剩余,则需要全部卸载。

if (j > oldEnd && j <= newEnd) {
  const anchorIndex = oldEnd + 1
  const anchor = oldChildren[anchorIndex]
    ? oldChildren[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 移动操作

如果前缀后缀处理完之后,并没有任何一组节点被处理完,则需要进行移动操作。我们根据 key 判断相同节点,然后找到每一个新节点在旧节点中的位置,我们把这个数组存为 source

function patchKeyedChildren(n1, n2, container) {
  // ...
  if (j > oldEnd && j <= newEnd) {
    // ...
  } else if (j > newEnd && j <= oldEnd) {
    // ...
  } else {
    const count = newEnd - j + 1 // 新节点剩余的数量
    const source = new Array(count)
    source.fill(-1) // 初始值全部设为 -1
    const oldStart = j
    const newStart = j

    for (let i = oldStart; i <= oldEnd; i++) {
      const oldVNode = oldChildren[i]
      for (let k = newStart; k <= newEnd; k++) {
        const newVNode = newChildren[k]
        if (oldVNode.key === newVNode.key) {
          patch(oldVNode, newVNode, container)
          source[k - newStart] = i
        }
      }
    }
  }
}

为了在后面操作中快速找到相同 key 的节点(而不是每次都需要遍历)可以使用 Map 存储 key 对应的节点位置。而对于找不到对于 key 的节点,则需要卸载。

const count = newEnd - j + 1 // 新节点剩余的数量
const source = new Array(count)
source.fill(-1) // 初始值全部设为 -1
const oldStart = j
const newStart = j
// 对新数组构建 key->index 索引表
const keyIndex = {}
for (let i = newStart; i <= newEnd; i++) {
  keyIndex[newChildren[i].key] = i
}

for (let i = oldStart; i <= oldEnd; i++) {
  const oldVNode = oldChildren[i]
  const k = keyIndex[oldVNode.key]
  if (typeof k !== 'undefined') {
    const newVNode = newChildren[k]
    patch(oldVNode, newVNode, container)
    source[k - newStart] = i
  } else {
    unmount(oldVNode)
  }
}

然后我们使用第九章类似的思路来判断是否有节点需要移动。同时记录 patch 过的节点数量,当 patch 过的元素等于新节点的数量,剩下的节点直接卸载。

const count = newEnd - j + 1 // 新节点剩余的数量
const source = new Array(count)
source.fill(-1) // 初始值全部设为 -1
const oldStart = j
const newStart = j
let moved = false
let pos = 0
// 对新数组构建 key->index 索引表
const keyIndex = {}
for (let i = newStart; i <= newEnd; i++) {
  keyIndex[newChildren[i].key] = i
}
let patched = 0
for (let i = oldStart; i <= oldEnd; i++) {
  const oldVNode = oldChildren[i]
  if (patched <= count) {
    const k = keyIndex[oldVNode.key]
    if (typeof k !== 'undefined') {
      const newVNode = newChildren[k]
      patch(oldVNode, newVNode, container)
      patched++
      source[k - newStart] = i
      if (k < pos) {
        moved = true
      } else {
        pos = k
      }
    } else {
      unmount(oldVNode)
    }
  } else {
    // 如果更新过的节点数量已经大于新的节点数量 说明剩下的节点都需要被卸载
    unmount(oldVNode)
  }
}

11.3 如何移动元素

我们上面通过 moved = true 标识了需要移动,下面该考虑如何移动。

我们先计算 source 的最长递增子序列。这部分不是重点,折叠了,但是我加了下注释。应该是力扣原题~

点击查看代码
function getSequence(arr) {
  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 (arr[j] < arrI) {
        p[i] = j
        result.push(i)
        continue
      }
      u = 0
      v = result.length - 1
      // 二分找到第一个大于arrI的位置
      // u最小值 v最大值
      while (u < v) {
        c = ((u + v) / 2) | 0 // 取中间值
        if (arr[result[c]] < arrI) {
          u = c + 1
        } else {
          v = c
        }
      }
      if (arrI < arr[result[u]]) {
        if (u > 0) {
          // 3 4 1
          // 我们开始会存储 3 4 <0,1>
          // 然后会存为 1 4 <2,1>
          // 但是要知道 4 在 1 之前 不可以这样构造子序列
          // 所以用p记录:一个下标被放入result时 它的前一个位置是哪个下标
          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
}

console.log(getSequence([1, 2, 3])) // [ 0, 1, 2 ]
console.log(getSequence([3, 4, 1])) // [ 0, 1 ]
console.log(getSequence([3, 2, 1])) // [ 2 ]

我们通过 getSequence 计算出最长递增子序列对应的下标数组。对于最长递增子序列中包含的下标对应的节点,不进行移动,对其他节点进行移动。

方法就是从子节点数组和 lis 数组的最后一个节点作比较,如果子节点的下标等于 lis 的值,就不移动节点,同时下标向前移动,否则移动节点。

if (moved) {
  const seq = lis(source)
  let s = seq.length - 1
  for (let i = count - 1; 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
      patch(null, newVNode, container, anchor)
    }
    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 {
      s--
    }
  }
} else {
  for (let i = count - 1; 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
      patch(null, newVNode, container, anchor)
    }
  }
}

P.S.书上没有写 moved=false 相关逻辑,我看了下还是应该写的。

posted @ 2023-02-06 17:26  我不吃饼干呀  阅读(36)  评论(0编辑  收藏  举报