渲染器
11.快速Diff算法
11.1相同的前置元素和后置元素
快速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 = 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移动操作
预处理后,对于剩余的未处理节点,需要判断哪些节点需要移动。
首先,构造一个数组source,它的长度等于新的一组子节点在经过预处理之后剩余未处理节点的数量,且初始值均为-1。source将用来存储新的一组子节点中的节点在旧的一组子节点中的位置索引,之后将会使用它计算出一个最长递增子序列,并用于辅助完成DOM移动的操作。
为了快速填充source,构建一张索引表,用来存储节点的key和节点位置索引之间的映射。
为了判断节点是否需要移动,新增两个变量moved和pos。前者的初始值为false,代表是否需要移动节点,后者的初始值为0,代表遍历旧的一组子节点的过程中遇到的最大索引值k。
function patchKeyedChildren(n1, n2, container) {
// ...
if (j > oldEnd && j <= newEnd) {
// ...
} else if (j > newEnd && j <= oldEnd) {
// ...
} else {
// 构造source数组
const count = newEnd - j + 1
const source = new Array(count).fill(-1)
const oldStart = j
const newStart = j
let moved = false
let pos = 0
// 构造索引表
const keyIndex = {}
for (let i = newStart; i <= newEnd; i++) {
keyIndex[newChildren[i].key] = i
}
let patched = 0
for (let i = oldStart; i <= oldEnd; i++) {
oldVNode = oldChildren[i]
if (patched <= count) {
const k = keyIndex[oldVNode.key]
if (typeof k !== 'undefined') {
newVNode = newChildren[k]
patch(oldVNode, newVNode, container)
source[k - newStart] = i
// 判断节点是否需要移动
if (k < pos) {
moved = true
} else {
pos = k
}
} else {
unmount(oldVNode)
}
} else {
unmount(oldVNode)
}
}
}
}
11.3如何移动元素
计算source数组的最长递增子序列,并返回该序列中的元素在source数组中的位置索引。最长递增子序列所指向的节点即为不需要移动的节点。
function patchKeyedChildren(n1, n2, container) {
// ...
if (j > oldEnd && j <= newEnd) {
// ...
} else if (j > newEnd && j <= oldEnd) {
// ...
} else {
// ...
if (moved) {
// 计算最长递增子序列
const seq = lis(source)
let s = seq.length - 1
let i = count - 1
for (; 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)
} 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
insert(newVNode.el, container, anchor)
} else {
// 该节点不需要移动
s--
}
}
}
}
}