- 作者:老汪软件技巧
- 发表时间:2024-08-21 17:02
- 浏览量:
react调和函数的作用就是在react fiber树构建过程中,比较旧的fiber和新的reactElement,尽量复用旧fiber的一个方法,也就是常说的diff算法
在reconcileChildFibers逻辑中,区分为子节点为单节点和多节点
function reconcileChildFibers(
returnFiber: Fiber,
currentFirstChild: Fiber | null,
newChild: any,
lanes: Lanes,
): Fiber | null {
// 处理单个子节点
if (typeof newChild === 'object' && newChild !== null) {
switch (newChild.$$typeof) {
case REACT_ELEMENT_TYPE:
return placeSingleChild(
reconcileSingleElement(
returnFiber,
currentFirstChild,
newChild,
lanes,
),
);
}
//处理多子节点
if (isArray(newChild)) {
return reconcileChildrenArray(
returnFiber,
currentFirstChild,
newChild,
lanes,
);
}
}
return deleteRemainingChildren(returnFiber, currentFirstChild);
}
单节点
function reconcileSingleElement(
returnFiber: Fiber,
currentFirstChild: Fiber | null,
element: ReactElement,
lanes: Lanes,
): Fiber {
const key = element.key;
let child = currentFirstChild;
while (child !== null) {
if (child.key === key) {
const elementType = element.type;
if (child.elementType === elementType) {
deleteRemainingChildren(returnFiber, child.sibling);
const existing = useFiber(child, element.props);
existing.return = returnFiber;
return existing;
}
deleteRemainingChildren(returnFiber, child);
break;
} else {
deleteChild(returnFiber, child);
}
child = child.sibling;
}
const created = createFiberFromElement(element, returnFiber.mode, lanes);
created.return = returnFiber;
return created;
}
从上面代码中可以看到,这里有一个常见的误区,react的diff算法比较的是fiber和reactElement,而不是fiber和fiber进行对比。可以看到这个方法主要由一个大的while循环构成,接下来详细解释下循环中做的事
进入while循环中,根据key和type的不同,又分为如下几种情况:
1.key相同,type不同
例如如下情况
这种情况说明元素的节点类型发生了变化,p---->div,直接调用deleteRemainingChildren(returnFiber, child) ,删除旧的p fiber及它后面的所有fiber (删除旧的p fiber 和div fiber),跳出循环,执行创建fiber的代码,此时就不会进入复用逻辑,
const created = createFiberFromElement(element, returnFiber.mode, lanes);
created.return = returnFiber;
2. key不同
执行deleteChild(returnFiber, child) ,此处就是删除key为1的div fiber , 然后执行child = child.sibling,此时child变成key为3的div fiber, 再次执行循环体
3.key相同,type相同
当key和type都相同,即会进入fiber复用逻辑,可以看到里面调用了useFiber方法,该方法会根据传入的props尽量复用旧的fiber
deleteRemainingChildren(returnFiber, child.sibling);
const existing = useFiber(child, element.props);
existing.return = returnFiber;
多节点
当新的children为多节点,会进入多节点reconcile流程,我们以下图为例
function reconcileChildrenArray(
returnFiber: Fiber,
currentFirstChild: Fiber | null,
newChildren: Array,
lanes: Lanes,
): Fiber | null {
let resultingFirstChild: Fiber | null = null;
let previousNewFiber: Fiber | null = null;
let oldFiber = currentFirstChild;
let lastPlacedIndex = 0;
let newIdx = 0;
let nextOldFiber = null;
//第一次循环,遍历前后最大长度,复用oldFiber,直到key不一样就跳出
for (; oldFiber !== null && newIdx < newChildren.length; newIdx++) {
if (oldFiber.index > newIdx) {
nextOldFiber = oldFiber;
oldFiber = null;
} else {
nextOldFiber = oldFiber.sibling;
}
//updateSlot方法内部就是去判断key是否相同,相同且type也相同的话就复用,key不同的话就返回null
const newFiber = updateSlot(
returnFiber,
oldFiber,
newChildren[newIdx],
lanes,
);
if (newFiber === null) {
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) {
resultingFirstChild = newFiber;
} else {
previousNewFiber.sibling = newFiber;
}
previousNewFiber = newFiber;
oldFiber = nextOldFiber;
}
//新数组被遍历完了,old Fiber没遍历的就可以删了
if (newIdx === newChildren.length) {
// We've reached the end of the new children. We can delete the rest.
deleteRemainingChildren(returnFiber, oldFiber);
return resultingFirstChild;
}
//旧的被遍历完了,但是新的还有,新的剩下的打上Placement标记
if (oldFiber === null) {
for (; newIdx < newChildren.length; newIdx++) {
const newFiber = createChild(returnFiber, newChildren[newIdx], lanes);
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;
}
return resultingFirstChild;
}
// Add all children to a key map for quick lookups.
const existingChildren = mapRemainingChildren(returnFiber, oldFiber);
// 第二次循环: 遍历剩余非公共序列, 优先复用oldFiber序列中的节点
// Keep scanning and use the map to restore deleted items as moves.
for (; newIdx < newChildren.length; newIdx++) {
const newFiber = updateFromMap(
existingChildren,
returnFiber,
newIdx,
newChildren[newIdx],
lanes,
);
if (newFiber !== null) {
if (shouldTrackSideEffects) {
if (newFiber.alternate !== null) {
//如果map中的fiber被复用了,则说明我们不需要给他打上deletion 标记,需要将它从map中删除(最终map中的fiber都是需要被删除的)
existingChildren.delete(
newFiber.key === null ? newIdx : newFiber.key,
);
}
}
lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);
if (previousNewFiber === null) {
resultingFirstChild = newFiber;
} else {
previousNewFiber.sibling = newFiber;
}
previousNewFiber = newFiber;
}
}
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));
}
return resultingFirstChild;
}
上面的源码中,我们对其中的关键步骤进行讲解。
首先会进入一次循环
本次循环最多会循环旧数组和新数组的最小长度次,以图中的例子来看就是5次
每次循环都会执行updateSlot方法,方法内部会判断新旧节点的key是否相同,这里会出现几种情况:
可以看出,第一次大的循环会一直往后遍历,尽最大可能的复用旧的节点,直到新旧节点的key不同,就会跳出循环,跳出第一次循环会产生3种不同的可能性
新数组被遍历完,旧的还有剩下的
这种情况只需要删除旧数组中剩下的即可,结束diff算法流程
旧数组被遍历完,新数组还有剩下的
这种情况说明旧数组都得到了复用,只需要给新数组中剩下的打上新增标记即可,结束diff算法流程
新旧数组都还有剩下的
这种情况是最复杂的,因此会进入diff算法后续流程
第一轮循环结束,新旧数组都还有剩余
首先会把旧数组中剩余的节点放入一个map中,以节点的key为键,方便后续通过key进行查找
接下来就会对剩余的新数组进行遍历,遍历过程中拿到每一个节点的key,去map里面找是否有能匹配上的节点,如果key和type都匹配上了则复用旧的节点,当节点得到了复用,需要从map中移除对应的旧节点,因为运行到最后map里面保留的就是所有没有得到复用的节点,需要打上删除标记
当一个节点得到了复用,这个节点所在的位置可能发生了变化,例如
A->B->C->D->E 变成 A->B->E->C->X->Y,其中C和E的相对位置发生了变化,react内部是如何确定节点的位置变化了呢?确定节点是否发生移动的方法就是placeChild
这里涉及到了一个关键的变量,lastPlacedIndex,这个变量的含义可以理解为目前已经复用的节点在旧数组中最大的index,读起来很拗口,通过上面的例子来解释:当遍历到了A和B节点时,发现可以被复用,则lastPlacedIndex被修改为B节点在旧数组中的index,也就是1,再往后遍历到了E,此时E的旧index为4,大于lastPlacedIndex,则此时E节点是相对旧数组没有发生移动的,因此不会给E节点标记为移动,且会把lastPlacedIndex设置为4。继续遍历到C,C的旧index为2,lastPlacedIndex为4,因此可以知道在旧数组中,当前遍历的节点在上一个节点之前,因此当前节点是需要移动的。
由此可以看出react对节点移动的策略是:让元素往后移动,避免元素往前移动
function placeChild(
newFiber: Fiber,
lastPlacedIndex: number,
newIndex: number,
): number {
newFiber.index = newIndex;
const current = newFiber.alternate;
if (current !== null) {
const oldIndex = current.index;
if (oldIndex < lastPlacedIndex) {
//应该是尽量让节点往后移动,避免节点往前移动
// This is a move.
newFiber.flags |= Placement;
return lastPlacedIndex;
} else {
// This item can stay in place.
return oldIndex;
}
} else {
// This is an insertion.
newFiber.flags |= Placement;
return lastPlacedIndex;
}
}