Diff算法的对比对象
组件重新渲染生成的Element VS 组件对应的oldFiber
两者是用不同的数据结构存储的。
Diff主要是针对多个子节点
的情况,单个子节点直接复用oldFiber即可,不需要对比了。
Diff的步骤
diff算法的关键函数是reconcileChildrenArray
如下图,可以看到,
参数是父级Fiber,第一个子节点Fiber,Element新生成的Children数组,更新优先级lane及debugger信息,
返回值是Fiber
第一步,遍历newChildren,复用oldFiber创建新Fiber
javascript
function reconcileChildrenArray(
returnFiber: Fiber,
currentFirstChild: Fiber | null,
newChildren: Array<any>,
lanes: Lanes,
debugInfo: ReactDebugInfo | null,
): Fiber | null {
let resultingFirstChild: Fiber | null = null;
let previousNewFiber: Fiber | null = null;
let oldFiber = currentFirstChild;
let lastPlacedIndex = 0;
let newIdx = 0;
let nextOldFiber = null;
// 第一步,遍历newChildren,复用oldFiber
for (; oldFiber !== null && newIdx < newChildren.length; newIdx++) {
if (oldFiber.index > newIdx) {
nextOldFiber = oldFiber;
oldFiber = null;
} else {
nextOldFiber = oldFiber.sibling;
}
const newFiber = updateSlot(
returnFiber,
oldFiber,
newChildren[newIdx],
lanes,
debugInfo,
);
if (newFiber === null) {
if (oldFiber === null) {
oldFiber = nextOldFiber;
}
break;
}
这里会通过sibling指针遍历oldFiber,找到与children对应的oldFiber。oldFiber树是从currentFirstChild开始比较,也就是说大家都是从第一个节点开始对比。
调用updateSlot,其内部会判断tag和key判断是否匹配,如果匹配,复用老的Fiber,形成新的Fiber,如果不匹配,返回null,newFiber为null。
第二步,假如newChildren全部遍历完,统一删除剩余的oldFiber
javascript
// 第二步,假如newChildren全部遍历完,则删除其余的Fiber
if (newIdx === newChildren.length) {
// We've reached the end of the new children. We can delete the rest.
deleteRemainingChildren(returnFiber, oldFiber);
if (getIsHydrating()) {
const numberOfForks = newIdx;
pushTreeFork(returnFiber, numberOfForks);
}
return resultingFirstChild;
}
第三步,oldFiber是null,再次遍历newChildren,创建新Fiber
ini
// 第三步,oldFiber是null,说明newChildren没在第一步顺利完成遍历,再次循环newChildren,创建新Fiber
if (oldFiber === null) {
// If we don't have any more existing children we can choose a fast path
// since the rest will all be insertions.
for (; newIdx < newChildren.length; newIdx++) {
const newFiber = createChild(
returnFiber,
newChildren[newIdx],
lanes,
debugInfo,
);
if (newFiber === null) {
continue;
}
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;
}
if (getIsHydrating()) {
const numberOfForks = newIdx;
pushTreeFork(returnFiber, numberOfForks);
}
return resultingFirstChild;
}
第四步,针对发生移动和更复杂情况
javascript
// 第四步,newChildren没遍历完且oldFiber不为null,针对移动等更复杂情况
// Add all children to a key map for quick lookups.
const existingChildren = mapRemainingChildren(oldFiber);
// Keep scanning and use the map to restore deleted items as moves.
for (; newIdx < newChildren.length; newIdx++) {//这里的newIndex是上回遍历的结果
const newFiber = updateFromMap(
existingChildren,
returnFiber,
newIdx,
newChildren[newIdx],
lanes,
debugInfo,
);
if (newFiber !== null) {
if (shouldTrackSideEffects) {
if (newFiber.alternate !== null) {
// The new fiber is a work in progress, but if there exists a
// current, that means that we reused the fiber. We need to delete
// it from the child list so that we don't add it to the deletion
// list.
existingChildren.delete(
newFiber.key === null ? newIdx : newFiber.key,
);
}
}
lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);
if (previousNewFiber === null) {
resultingFirstChild = newFiber;
} else {
previousNewFiber.sibling = newFiber;
}
previousNewFiber = newFiber;
}
}
mapRemainingChildren方法返回一个map,名为existingChildren
,map的结构是[老fiber对应的key(或index),老fiber]
。
接下来遍历没有处理的children,通过updateFromMap,判断mapRemainingChildren中有没有可以复用的oldFiber,如果有,那么复用,如果没有,新创建一个newFiber。
复用的oldFiber会从existingChildren
中被删除。
第五步,统一删除没有复用的oldFiber
javascript
// 第五步,删除没有复用的oldFiber
if (shouldTrackSideEffects) {
// Any existing children that weren't consumed above were deleted. We need
// to add them to the deletion list.
existingChildren.forEach(child => deleteChild(returnFiber, child));
}
if (getIsHydrating()) {
const numberOfForks = newIdx;
pushTreeFork(returnFiber, numberOfForks);
}
return resultingFirstChild;