巨人の肩の上に立ってvue3-を見る-第11章高速差分アルゴリズム

巨人の肩の上に立ってVueを見て、HuoChunyangのVueの設計と実装。著者は、vue3の最下層の実装を質問の形で段階的に解釈します。非常に独創的な発想で、本の主な論理を直列につなぎ、読んだ後の記録にもなりますので、この形でみんなとコミュニケーションを取り、学ぶことができればと思います。

オープニング

高速アルゴリズムはvue3で使用され、テキストDiffの前処理を利用して、最初に古いノードと新しいノードの同じ位置で前ノードと後ノードを処理します。フロントノードとバックノードがすべて処理される場合、次の3つの状況があります。

同じプレノードとポストノードが処理された後、古いノードはすべて処理され、新しいノードはまだ存在します。この時点で、それらを追加する必要があり、残りの新しいノードはトラバースされ、次のノードの前に挿入されます。ノード。

同様に、同じプレノードとポストノードが処理された後、すべての新しいノードが処理され、古いノードはそのまま残ります。このとき、アンインストールして古いノードをトラバースしてアンインストールする必要があります。

2つのグループに未処理のノードがまだある場合は、どちらを追加する必要があり、どちらをアンインストールする必要があるかを確認する必要があります。Simple Diff Algorithmの章で説明したように、古いノードをトラバースして最大インデックス値を見つけます。新しいノードのキーが増えていない場合は、移動する必要があるノードであることを意味します。

このとき、古いノードに新しいノードの位置を格納する配列ソースを作成できます。値が-1の場合、これは新しいノードであることを意味します。

次に、sourcerの最大増加部分列を見つけ、テールからトラバースします。テールノードとソースの値が等しくない場合は、それを移動する必要があることを意味し、パッチを呼び出して移動します。それ以外の場合は、必要なだけです。すべてのトラバーサルが完了するまで、sが次の位置を指すようにします。

これは、FastDiffアルゴリズムのプロセスです。

11.1。同じ前要素と後要素

のノードの新旧のセットの位置は同じままであり、それらを移動する必要はありません。whileループは、すべて同じプレノードとポストノードを検出し、パッチ関数を呼び出して、ノードが異なるキー値で配置されるまで更新します。

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:同じプレノードとポストノードが処理された後、古いノードはすべて処理され、新しいノードはまだ(新しい)です

条件が満たされました

  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