简单Diff算法
之前没有Diff算法时候的写法是卸载所以旧节点挂载所有新节点,那样的化DOM操作太多。 在简单Diff算法中
- 通过两层for循环,遍历新旧子节点,通过
key来将新旧节点对应起来,然后进行移动 - 如果遍历过程中,新节点中有的key在旧节点中不存在,那就执行挂载操作;
- 如果有旧节点的key在新节点中不存在,则执行卸载操作
如何判断移动? 拿新的一组子节点中的节点去旧的一组子节点中寻找可复用的节点,如果找到了,记录该节点的位置索引,称为最大索引,在整个更新过程中,如果有节点的索引小于最大索引,那么说明这个节点对应的真实DOM需要移动。
双端Diff算法(Vue2)
每一轮都进行下面这样的对比操作,能找到可复用的节点则进行patch和移动等操作
**非理想情况:**在一轮比较中,四次都无法命中可复用节点,则可在oldVNode中寻找newStartIdx的可复用节点,然后进行patch和移动,并把oldVNode中,与newStartIdx对应的节点设为undefined(因为该节点已经移动到别处了)
**添加节点:**某个新节点即使在oldVNode中遍历也找不到,那就是新的节点,循环结束后,遍历newStartIdx和newEndIdx之间的节点,就可以完成对新增元素的处理。
**移除不存在的元素:**与新增类似,遍历oldStartIdx和oldEndIdx之间的节点,即可逐一卸载。
快速Diff算法(Vue3)
快速Diff借鉴了纯文本Diff算法,
**source数组:**该数组的长度为新的一组子节点去掉相同的前置、后置节点之后,未处理的节点的长度。用来储存一组子节中的节点在旧的一组子节点中的位置索引,后面将会使用它计算一个最长递增子序列,并用于辅助完成DOM移动的操作。
但是两次循环来构建source数组的时间复杂度(O(n1*n2))还是高,所以使用索引表来填充source数组
**索引表:**用来存储节点的key和节点位置索引之间的映射。
快速Diff算法判断节点是否需要移动的方法和简单Diff算法类似
创建moved作为标识,它的值为true时,说明需要DOM移动操作,构建source数组,用于DOM移动操作。
source数组的最长递增子序列用seq表示,并用变量s指向seq的最后一个位置,变量i指向source的最后一个位置。
然后,更新过程分为三个步骤:
- 判断source[i]的值是否为-1,如果是-1,则为全新节点,进行挂载,如果不是,进行下一步
- 判断
i !== seq[s],if true,则该节点需要移动,else,第三步 - 到了第三步,该节点不需要移动,但如果s不为0,则需要进行
s--操作,指向下一个位置
if(moved) {
const seq = lis(sources)
// s指向最长递增子序列的最后一个元素
let s = seq.length - 1
let i = count - 1
for(i; i >= 0; i--) {
if(source[i] === -1) {
// 是全新节点,需要挂载
// 该节点在新 children 中的真实位置索引
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 {
// 当 i === seq[s]时,说明该位置的节点不需要移动
// 并让s指向下一个位置
s--
}
}
}
最长的递增子序列
获取给定序列最长的递增子序列的方法:Vue3采用的是贪心 + 二分查找:O(nlogn)
function getSequence(arr) {
/* p 的作用:回溯:使用前驱索引纠正最长递增子序列的偏差
数组每一项保存应该排在当前元素前面元素的下标。
然后经过逆序遍历数组 p,纠正 result 数组的元素 */
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 (arrI > arr[j]) {
// 如果数组当前的数比result里面索引的最后一项大
// 则把当前索引也存入result
result.push(i)
p[i] = j
continue
}
// 要开始二分了
/*
二分找到某一项刚好大于当前项,此时 u 和 v 指针应
该是指向同一个元素下标,然后用当前元素替换掉二分找到的那一项。
*/
u = 0 // 左
v = result.length - 1 // 右
while (u < v) {
c = ((u + v) / 2) | 0 // 中
// 虽然arr是无序的,但是arr[result[?]]是有序的
if (arr[result[c]] < arrI) {
u = c + 1
} else {
v = c
}
}
// while 循环结束后,u 和 v 会指向同一个元素
if (arrI < arr[result[u]]) {
if (u > 0) {
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
}
let nums = [3, 2, 8, 9, 5, 6, 7, 11, 15, 4]
console.log(getSequence(nums).map(e => nums[e]))