多节点 Diff
思路
- 比对双方的 currentFiber 的 children ,是一个链表结构,另一方是 jsx 对象中的 children,是一个对象数组。
- 每一次都只能从 jsx 中取一个 child 和当前的 fiber 做比对
- 由于数据统计等原因, React 对更新处理的优先级高于新增/删除操作,所以在比对的时候,首先看能否复用更新,不行再做另外的操作
- 整个 Diff 算法分两次遍历处理,第一次是为了遍历出可以更新的节点,第二次是为了处理其他不许与更新的节点。
第一次更新
第一次遍历的具体代码:
/**
* @分析 : 第一轮遍历
* 1.遍历 newChildren, 用 newChildren[i] 与 oldFiber 进行比较,判断是否可以复用
*/
for (; oldFiber !== null && newIdx < newChildren.length; newIdx++) {
if (oldFiber.index > newIdx) {
nextOldFiber = oldFiber;
oldFiber = null;
} else {
nextOldFiber = oldFiber.sibling;
}
// 1 VS 1 对比,生成 newFiber
const newFiber = updateSlot(
returnFiber,
oldFiber,
newChildren[newIdx],
lanes,
);
if (newFiber === null) {
if (oldFiber === null) {
oldFiber = nextOldFiber;
}
break;
}
if (shouldTrackSideEffects) {
if (oldFiber && newFiber.alternate === null) {
// We matched the slot, but we didn't reuse the existing fiber, so we
// need to delete the existing child.
deleteChild(returnFiber, oldFiber);
}
}
lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);
if (previousNewFiber === null) {
// TODO: Move out of the loop. This only happens for the first run.
resultingFirstChild = newFiber;
} else {
previousNewFiber.sibling = newFiber;
}
previousNewFiber = newFiber;
oldFiber = nextOldFiber;
}
遍历过程中,fiber 和单个节点的更新过程 -- 和单节点元素diff 很像
// 第一个遍历时,oldFiber 和 newChild 对比 ,只是 1 VS 1 的比较
// 有点类似单节点元素diff,但是这里不会随便就把 oldFiber 给干掉
function updateSlot(
returnFiber,
oldFiber,
newChild,
lanes
) {
const key = oldFiber !== null ? oldFiber.key : null
if (typeof newChild === 'string' || typeof newChild === 'number') {
if (key !== null) {
return null
}
return updateTextNode(returnFiber, oldFiber, "" + newChild, lanes)
}
if (typeof newChild === 'object' && newChild !== null) {
switch (newChild.$$type) {
case REACT_ELEMENT_TYPE:
if (newChild.key === key) {
if (newChild.type === REACT_FRAGMENT_TYPE) {
xxx
}
return updateElement(returnFiber, oldFiber, newChild, lanes)
} else {
return null
}
case REACT_PORTAL_TYPE: xxx
}
}
// child 是数组或者类数组
if (isArray(newChild) || getIteratorFn(newChild)) {
if (key !== null) {
return null
}
return updateFragment(returnFiber, oldFiber, newChild, lanes, null);
}
}
Diff 步骤
- 遍历 jsx 的对象数组,想 children[i] 和 oldFiber 比较,判断是否可以复用
- 如果可以,i++, oldFiber.sibling, 继续走下去
- 如果不可以复用
- key 不同导致的不可复用,直接跳出第一次循环,也就是说更新查找结束
- 如果是 type 类型的不同导致的不可复用,将 oldFiber 标记为 DELETE,继续遍历
- children 遍历完了,或者 oldFiber 走完了,退出循环
第一次循环结束会出现的结果
- 步骤3跳出的,也就是没有走完就退出循环,children 和 oldFiber 都没有遍历完
- 步骤4跳出的,可能是某一遍走完,也有可能是一起走完的
第二轮遍历
- 因为我们是通过 children 和 oldFiber 比对的,但是最重要还是 children,所以主要分成三种情况
- children 没有了,那么剩下的都是删除操作了
- children 还有,oldFiber 没有了,那么剩下的都是新增操作
- children 还有,oldFiber 也还有,那么需要第二次遍历来解决了
children没有了
// jsx 对象数组已经结束 -- 证明剩下都是删除操作了
if(newIdx === newChildren.length){
// jsx 的对象数组遍历完了,那么就将剩下的 oldFiber 删除掉吧
deleteRemainingChildren(returnFiber,oldFiber)
return resultingFirstChild
}
children 还有,但是 oldFiber 没有了
// oldFiber 遍历完了,但是 newChildren 没有遍历完,说明有节点插入了
if (oldFiber === null) {
for (; newIdx < newChildren.length; newIdx++) {
const newFiber = createChild(returnFiber, newChildren[newIdx], lanes)
if (newFiber === null) {
continue
}
lastPlaceIndex = placeChild(newFiber, lastPlaceIndex, newIdx)
// 开始将 newFiber 链接到 WIPFiber 树中去了
if (previousNewFiber === null) {
// 只有在初始状态才会酱紫,更新阶段不会
// 最后返回的就是 newFiber
resultingFirstChild = newFiber
} else {
previousNewFiber.sibling = previousNewFiber
}
previousNewFiber = newFiber
}
return resultingFirstChild
}
两者都还存在
- 先把 fiber 中的值用 map 存起来
- 遍历 children,然后根据再做对比,主要是找出可能是key交换的情况,也就是用 child 和整个 map 进行比对,看看是否能废物利用,直接替换,不要做删除新增操作
- 每次比对后,看一下新的 fiber 是否有 sibling,有就是怎么是复用了的,那么就要从 map 中抽出来,以免最后用删除新增的方式进行更新
- 遍历结束后,还存在于 map 中的节点就是 DELETE 掉的了
- 最后返回 fiber
// 两者都还存在 fiber
// 将未处理的 fiber ,以 fiber.key 为 key,以 fiber 为 value 保存在一个 map 中
const existingChildren = mapRemaingChildren(returnFiber, oldFiber)
for (; newIdx < newChildren.length; newIdx++) {
const newFiber = updateFromMap(
existingChildren,
returnFiber,
newIndex,
newChildren[newIdx],
lanes
)
if (newFiber !== null) {
if (shouldTrackSideEffects) {
if (newFiber.alternate !== null) {
// 存在 alternate,证明这个 newFiber 和 currentFiber 是一致的
// 所以应该直接在 existingChildren 中删除这个节点,防止现在 oldFiber 中删除了该节点,然后又重新造一个
existingChildren.delete(
newFiber.key === null ? newIdx : newFiber.key
)
}
}
// 最后一个可复用的节点再 oldFiber 中的位置索引
lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);
if (previousNewFiber === null) {
resultingFirstChild = newFiber;
} else {
previousNewFiber.sibling = newFiber;
}
previousNewFiber = newFiber;
}
}
if(shouldTrackSideEffects){
// 其他还在 existingChildren 中的值,需要在删除
existingChildren.forEach(child => deleteChild(returnFiber, child));
}
return resultingFirstChild;