单节点 diff
单节点 diff 的核心逻辑在 reconcileSingleElement 方法中
reconcileSingleElement的返回值直接通过 placeSingleChild 包裹,而 placeSingleChild 只是会给 diff 时创建的新节点打上 Placement 标记(更新过程中)
function reconcileSingleElement(
returnFiber: Fiber,
currentFirstChild: Fiber | null,
element: ReactElement,
lanes: Lanes,
debugInfo: ReactDebugInfo | null,
): Fiber {
// 当前element key
const key = element.key;
let child = currentFirstChild;
// 初次渲染时child 为 null,更新时不为null,进入对比逻辑
while (child !== null) {
// 如果element key和原fiber key 相同的话考虑复用
if (child.key === key) {
const elementType = element.type;
// 如果element类型为Fragment
if (elementType === REACT_FRAGMENT_TYPE) {
// 如果child的类型也为Fragment
if (child.tag === Fragment) {
// 因为已经匹配到了,就删除其他兄弟节点
deleteRemainingChildren(returnFiber, child.sibling);
// 使用 useFiber 复用当前fiber, props为element的children
const existing = useFiber(child, element.props.children);
existing.return = returnFiber;
return existing;
}
} else {
// 如果两者的type也相同
if (child.elementType === elementType) {
// 因为已经匹配到了,就删除其他兄弟节点
deleteRemainingChildren(returnFiber, child.sibling);
// 使用 useFiber 复用当前fiber, props为element的children
const existing = useFiber(child, element.props);
// 将element的ref绑定到复用完成的fiber上
coerceRef(returnFiber, child, existing, element);
existing.return = returnFiber;
return existing;
}
}
// 复用完成之后原fiber剩余节点(因为已经有新的fiber了)
deleteRemainingChildren(returnFiber, child);
break;
}
// 如果 key 不相同直接删除当前节点,后面的逻辑再去创建
else {
deleteChild(returnFiber, child);
}
child = child.sibling;
}
// 后面都是创建逻辑
if (element.type === REACT_FRAGMENT_TYPE) {
// 创建Fragment
const created = createFiberFromFragment(
element.props.children,
returnFiber.mode,
lanes,
element.key,
);
created.return = returnFiber;
return created;
} else {
// 创建Fiber
const created = createFiberFromElement(element, returnFiber.mode, lanes);
coerceRef(returnFiber, currentFirstChild, created, element);
created.return = returnFiber;
return created;
}
}
多节点 diff
多节点 diff 核心的逻辑在reconcileChildrenArray方法中
diff的本质是右移策略:如果某个节点往前移动了,相当于他之前的节点都要往右(后)移
具体实现:通过lastPlacedIndex判断节点是不是应该右移,直接举例子:
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;
// 遍历新节点列表
for (; oldFiber !== null && newIdx < newChildren.length; newIdx++) {
// 获取下一个老节点
if (oldFiber.index > newIdx) {
nextOldFiber = oldFiber;
oldFiber = null;
} else {
nextOldFiber = oldFiber.sibling;
}
// 开始进行单节点diff
const newFiber = updateSlot(
returnFiber,
oldFiber,
newChildren[newIdx],
lanes,
debugInfo,
);
// 如果newFiber为null则是diff时发现key不同不能复用,直接跳出循环
if (newFiber === null) {
if (oldFiber === null) {
oldFiber = nextOldFiber;
}
break;
}
if (shouldTrackSideEffects) {
if (oldFiber && newFiber.alternate === null) {
deleteChild(returnFiber, oldFiber);
}
}
// 判断当前节点是否需要移动,需要则标记Placement,不需要则返回新的lastPlacedIndex
lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);
// 如果previousNewFiber为null,则当前newFiber是第一个,直接赋值给resultingFirstChild
if (previousNewFiber === null) {
resultingFirstChild = newFiber;
} else {
// 如果previousNewFiber存证,则把当前newFiber拼接到previousNewFiber的sibling上
previousNewFiber.sibling = newFiber;
}
previousNewFiber = newFiber;
oldFiber = nextOldFiber;
}
// 如果新节点列表遍历完了,就直接删除老节点列表剩下的fiber,然后返回resultingFirstChild
if (newIdx === newChildren.length) {
deleteRemainingChildren(returnFiber, oldFiber);
return resultingFirstChild;
}
// 如果当前老节点列表遍历完了,则新的列表不需要diff,直接创建
if (oldFiber === null) {
for (; newIdx < newChildren.length; newIdx++) {
const newFiber = createChild(
returnFiber,
newChildren[newIdx],
lanes,
debugInfo,
);
if (newFiber === null) {
continue;
}
// 创建完之后也需要调用placeChild判断节点是否需要移动
lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);
if (previousNewFiber === null) {
resultingFirstChild = newFiber;
} else {
previousNewFiber.sibling = newFiber;
}
previousNewFiber = newFiber;
}
return resultingFirstChild;
}
// 逻辑走到这里,说明新老节点都存在,且不能复用
// 使用Map存储当前的老节点列表,key为节点的key,value为节点
const existingChildren = mapRemainingChildren(oldFiber);
// 遍历新节点列表
for (; newIdx < newChildren.length; newIdx++) {
// 判断新节点是否存在于老节点的Map中,存在就进行复用
const newFiber = updateFromMap(
existingChildren,
returnFiber,
newIdx,
newChildren[newIdx],
lanes,
debugInfo,
);
if (newFiber !== null) {
if (shouldTrackSideEffects) {
if (newFiber.alternate !== null) {
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) {
existingChildren.forEach(child => deleteChild(returnFiber, child));
}
return resultingFirstChild;
}
图解分析
简单情况
老列表称为old(fibers),新列表称为new(children, reactElement)
从new的第一项c节点开始,lastPlacedIndex的初始值为0:
- c节点在old中的索引(oldIndex)为2,oldIndex(2) > lastPlacedIndex,所以不给c打标,并且lastPlacedIndex赋值为2
- f节点的oldIndex为4,oldIndex(4) > lastPlacedIndex(2),所以不给f打标,并且lastPlacedIndex赋值为4
- a节点的oldIndex为0,oldIndex(0) < lastPlacedIndex(4),所以给a打标(要右移),lastPlacedIndex不变
- e节点的oldIndex为3,oldIndex(3) < lastPlacedIndex(4),所以给e打标(要右移),lastPlacedIndex不变
- b节点的oldIndex为1,oldIndex(1) < lastPlacedIndex(4),所以给b打标(要右移),lastPlacedIndex不变
所以标记之后的结果为:
全部右移到最右边
特别一点的情况
- b节点在old中的索引(oldIndex)为1,oldIndex(1) > lastPlacedIndex(0),所以不给b打标,并且lastPlacedIndex赋值为1
- a节点的oldIndex为0,oldIndex(0) < lastPlacedIndex(1),所以给a打标
- f节点的oldIndex为4,oldIndex(4) > lastPlacedIndex(1),lastPlacedIndex赋值为4
- c节点的oldIndex为2,oldIndex(2) < lastPlacedIndex(4),所以给c打标
- e节点的oldIndex为3,oldIndex(3) < lastPlacedIndex(4),所以给e打标
所以打标结果为:
所以是右移到第一个没有打标的元素之前