巨人の肩の上に立って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:同じプレノードとポストノードが処理された後、古いノードはすべて処理され、新しいノードはまだ(新しい)です
条件が満たされました
- oldEnd<jが成り立ちます。古いノードが処理されます
- 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:相同的前后置节点处理完之后之后,新的节点都完全处理了,旧的节点还有(删除)
满足的条件
- j ≤ oldEnd 成立。 旧节点走还有,需要删除
- 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:在新旧节点种有新增和需要删除的情况
- 构造一个数组source,长度等于新节点预处理过之后的节点的数量,初始值都是 -1,用来存储新节点在旧节点中的位置索引
- 为新节点构建一张索引表keyIndex,存储节点的key和位置索引之间的映射
- 遍历旧的节点,旧节点的k去索引表中查找新节点的位置,存储在k中
- 如果有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)
}
}
复制代码
- 如何判断节点需要移动
遍历旧节点过程遇到的索引值呈现递增趋势,说明不需要移动节点,反之需要。定义一个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)
}
}
复制代码
- 新增一个数量标识,代表已经更新过的节点数量,如果更新过的节点数量比新节点需要更新的数量多时,说明有多余节点需要删除
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--
}
}
复制代码