目标
要了解多节点 DIFF,首先需要知道已知条件和实现目标。
已知条件:
- 老 fiber
- 新的虚拟 DOM 节点数组
目标:根据新的虚拟 DOM 生成的新的 fiber 链。
第一轮遍历
对于前三种情况,只需要同时遍历新老节点,判断每个对应节点是否可以复用,如果可以复用,则复用并继续遍历下一个节点,如果不能复用则退出当前遍历。
在这次遍历结束后,判断新节点是否遍历完,如果新节点遍历完,则删除剩下的所有老节点。
如果老节点已遍历完成而新节点未遍历完成,则插入所有剩下的新节点。
第二轮遍历
在第四幅图中,为了尽可能复用老节点,需要移动老节点。
为了方便理解,上图中节点一一对应,只是打乱了顺序。为了确定老 fiber 节点是否需要移动,只需要确定老 fiber 中的相对顺序符合新 fiber 中的相对顺序,如上图中 B、C 节点,新老节点中都是 B 节点在 C 节点前,所有 B、C 节点不需要移动。
可以引入一个变量,标记上一个不需要移动的老 fiber 节点的下标。当遍历新节点时,找到对应的老 fiber,并获取其下标,如果其下标大于记录的上一个不需要移动的老节点下标,则更新不需要移动的老 fiber 节点的下标变量。如果小于,则更新 fiber 链表,并继续遍历。
遍历结束后,会得到一个根据新的虚拟 DOM 生成的新 fiber 链表,其顺序和新虚拟 DOM 顺序一致。最后返回该虚拟 DOM。
对应代码:
function reconcileChildrenArray(returnFiber, currentFirstChild, newChildren) {
// 返回的第一个新儿子
let resultingFirstChild = null;
// 上一个新 fiber
let previousNewFiber = null;
let newIdx = 0; // 用来遍历新的虚拟 DOM 的索引
let oldFiber = currentFirstChild; // 第一个老 fiber
let nextOldFiber = null; // 下一个老 fiber
let lastPlacedIndex = 0; // 上一个不需要移动的老 fiber 的索引
// 开始第一轮循环,如果老 fiber 有值,新的 fiber 也有值
for (; oldFiber !== null && newIdx < newChildren.length; newIdx++) {
// 暂存下一个老 fiber
nextOldFiber = oldFiber.sibling;
// 试图复用老 fiber
const newFiber = updateSlot(returnFiber, oldFiber, newChildren[newIdx]);
if (!newFiber) {
break;
}
if (shouldTrackSideEffects) {
// 如果有老 fiber,但是新 fiber 并没有成功复用老 fiber 和老的真实 DOM,那就删除老 fiber
// 在提交阶段删除真实 DOM
if (oldFiber && newFiber.alternate === null) {
deleteChild(returnFiber, oldFiber);
}
}
lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);
if (previousNewFiber === null) {
resultingFirstChild = newFiber;
} else {
previousNewFiber.sibling = newFiber;
}
previousNewFiber = newFiber;
oldFiber = nextOldFiber;
}
// 如果新的 fiber 已经遍历完,则删除剩余的 oldFiber
if (newIdx === newChildren.length) {
deleteRemainingChildren(returnFiber, oldFiber);
}
// 如果老的 fiber 遍历完成,新 fiber 还没,则开始插入逻辑
if (oldFiber === null) {
for (; newIdx < newChildren.length; newIdx++) {
const newFiber = createChild(returnFiber, newChildren[newIdx]);
if (newFiber === null) {
continue;
}
lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);
if (previousNewFiber === null) {
// 如果previousNewFiber 为 null,则说明这是第一个子 fiber
resultingFirstChild = newFiber;
} else {
previousNewFiber.sibling = newFiber;
}
previousNewFiber = newFiber;
}
}
// 开始处理移动的情况
const existingChildren = mapRemainingChildren(returnFiber, oldFiber);
// 开始遍历剩下的虚拟 DOM 子节点
for (; newIdx < newChildren.length; newIdx++) {
const newFiber = updateFromMap(existingChildren, returnFiber, newIdx, newChildren[newIdx]);
if (newFiber !== null) {
if (shouldTrackSideEffects) {
if (newFiber.alternate !== null) {
existingChildren.delete(newFiber.key || newIdx);
}
}
}
lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);
if (previousNewFiber === null) {
// 如果previousNewFiber 为 null,则说明这是第一个子 fiber
resultingFirstChild = newFiber;
} else {
previousNewFiber.sibling = newFiber;
}
previousNewFiber = newFiber;
}
if (shouldTrackSideEffects) {
existingChildren.forEach((child) => deleteChild(returnFiber, child));
}
return resultingFirstChild;
}