站在巨人的肩膀上看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:相同的前后置节点处理完之后之后,旧的节点都全部处理了,新的节点还有(新增)
满足的条件
- 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--
}
}
复制代码