双端Diff算法
双端Diff算法指的是,在新旧两组子节点的四个端点之间分别进行比较,并试图找到可复用的节点。相比简单Diff算法,双端Diff算法的优势在于,对于同样的更新场景,执行的DOM移动操作次数更少。
- 简单 Diff 算法(单向 Diff):
-
工作原理:简单 Diff 算法从一个序列的起始位置开始,逐个比较元素,找出不同之处。
-
特点:
-
顺序性:仅从一个方向进行比较,一旦发现不同之处,就会停止继续比较。
-
复杂度:时间复杂度为 O(n),其中 n 是序列的长度。
-
结果不唯一:因为只按照一个方向进行比较,可能会忽略一些潜在的更优的匹配方式。
-
- 双端 Diff 算法(双向 Diff):
- 工作原理:双端 Diff 算法同时从两个序列的起始位置开始,向中间移动,逐个比较元素,找出不同之处。
- 特点:
- 双向性:同时从两个方向进行比较,可以更全面地考虑匹配情况。
- 优化效率:通过跳过相同的前缀和后缀部分,减少了比较的次数,提高了效率。
- 复杂度:时间复杂度为 O(n+m),其中 n 和 m 分别是两个序列的长度。
- 结果更准确:考虑了更多的匹配可能性,得到更准确的差异结果。
双端Diff算法的比较方式
双端Diff算法是一种同时对新旧两组子节点的两个端点进行比较的算法,因此我们需要四个索引值,分别指向新旧两组节点的端点。
function patchChildren(n1, n2, container) {
if (typeof n2.children === 'string') {
// 省略部分代码
} else if (Array.isArray(n2.children)) {
// 封装 patchKeyedChildren 函数处理两组子节点
patchKeyedChildren(n1, n2, container);
} else {
// 省略部分代码
}
}
function patchKeyedChildren(n1, n2, container) {
const oldChildren = n1.children;
const newChildren = n2.children;
// 四个索引值
let oldStartIdx = 0;
let oldEndIdx = oldChildren.length - 1;
let newStartIdx = 0;
let newEndIdx = newChildren.length - 1;
// 四个索引指向的 vnode 节点
let oldStartVNode = oldChildren[oldStartIdx];
let oldEndVNode = oldChildren[oldEndIdx];
let newStartVNode = newChildren[newStartIdx];
let newEndVNode = newChildren[newEndIdx];
}
上面的代码中,我们将两组子节点的打补丁工程封装到了 patchKeyedChildren 函数中。该函数中,先获取两组新旧子节点 oldChildren 和 newChildren,然后创建四个索引值,分别指向新旧两组子节点的收尾,即 oldStartIdx(简称为旧前)、oldEndIdx(简称为旧后)、newStartIdx(简称为新前)、newEndIdx(简称为新后),以及四个索引值对应的 vnode。其中 oldStartVNode 和 oldEndVNode 是旧的一组子节点的第一个节点和最后一个节点,newStartVNode 和 newEndVNode 则是新的一组子节点的第一个子节点和最后一个子节点。
双端比较,每一轮均分为四个步骤:
- 比较旧的一组子节点的第一个子节点p-1(简称为旧前)于新的一组子节点的第一个子节点p-4(简称为新前),看看他们是否相同。由于两者的 key 值不同,因此不相同,不可复用,于是什么都不做。
- 比较旧的一组子节点的最后一个子节点p-4(简称为旧后)于新的一组子节点的最后一个子节点p-3(简称为新后),看看他们是否相同。由于两者的 key 值不同,因此不相同,不可复用,于是什么都不做。
- 比较旧的一组子节点的第一个子节点p-1(简称为旧前)于新的一组子节点的最后一个子节点p-3(简称为新后),看看他们是否相同。由于两者的 key 值不同,因此不相同,不可复用,于是什么都不做。
- 比较旧的一组子节点的最后一个子节点p-4(简称为旧后)于新的一组子节点的第一个子节点p-4(简称为新前)。由于他们的 key 相同,因此可以进行DOM复用。
四个步骤命中任何一步均说明命中的节点可以进行DOM复用,因此后续只需要进行DOM移动操作完成更新即可。
function patchKeyedChildren(n1, n2, container) {
const oldChildren = n1.children;
const newChildren = n2.children;
// 四个索引值
let oldStartIdx = 0;
let oldEndIdx = oldChildren.length - 1;
let newStartIdx = 0;
let newEndIdx = newChildren.length - 1;
// 四个索引指向的 vnode 节点
let oldStartVNode = oldChildren[oldStartIdx];
let oldEndVNode = oldChildren[oldEndIdx];
let newStartVNode = newChildren[newStartIdx];
let newEndVNode = newChildren[newEndIdx];
while(oldEndIdx >= oldStartIdx && newEndIdx >= newStartIdx) {
if (oldStartVNode.key === newStartVNode.key) {
// 步骤一:oldStartVNode 和 newStartVNode 比较
// 调用 patch 函数在 oldStartIdx 和 newStartIdx 之间打补丁
patch(oldStartVNode, newStartVNode, container);
// 更新相关索引到下一个位置
oldStartVNode = oldChildren[++oldStartIdx];
newStartVNode = newEndVNode[++newEndIdx];
} else if(oldEndVNode.key === newEndVNode.key) {
// 步骤二:oldEndVNode 和 newEndVNode 比较
// 节点在新的顺序中仍然处于尾部,不需要移动,但仍需打补丁
patch(oldEndVNode, newEndVNode, container);
// 更新索引和头尾部节点变量
newEndVNode = newChildren[--newEndIdx];
oldEndVNode = oldChildren[--oldEndIdx];
} else