Appearance
针对有 多个同级节点 的情况
多节点变化:
- 节点更新
- 节点属性更新(key,props)
- 节点类型(div -> p)
- 节点数量新增或者减少
- 节点位置顺序变化
Diff 算法
经历两轮遍历: 第一轮:处理需要更新的节点 第二轮:处理其他类型节点
reconcileChildrenArray(第一轮循环)
js
function reconcileChildrenArray(returnFiber, currentFirstChild, newChildren) {
// 初始化变量
let resultingFirstChild = null // 最终会返回给 wip 的childFiber
let previousNewFiber = null // 更新过程中记录上个Fiber(用于sibling连接)
let oldFiber = currentFirstChild; // 旧列表当前节点
let lastPlacedIndex = 0; // 旧节点列表循环到的index,用于插入新节点定位
let newIdx = 0; // 新节点列表的index
let nextOldFiber = null;
// 第一轮循环,处理节点更新
for (; oldFiber !== null && newIdx < newChildren.length; newIdx++) {
if (oldFiber.index > newIdx) {
nextOldFiber = oldFiber;
oldFiber = null;
} else {
nextOldFiber = oldFiber.sibling;
}
// 判断 DOM 节点能否复用
const newFiber = updateSlot(
returnFiber,
oldFiber,
newChildren[newIdx],
lanes,
);
if (newFiber === null) {
if (oldFiber === null) {
oldFiber = nextOldFiber;
}
// DOM 不能复用,跳出第一轮循环
// 从 updateSlot 来看: key 不相同则会跳出第一轮循环
// key不相同,属于位置变化,不属于节点更新
break;
}
if (shouldTrackSideEffects) {
// type 不相同,标记删除旧节点
if (oldFiber && newFiber.alternate === null) {
deleteChild(returnFiber, oldFiber);
}
}
// 标记插入新节点
lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);
if (previousNewFiber === null) {
resultingFirstChild = newFiber; // 更新过程创建的第一个Fiber
} else {
previousNewFiber.sibling = newFiber; // 通过sibling连接下一个节点
}
previousNewFiber = newFiber; // 更新成新创建的节点
oldFiber = nextOldFiber;
}
}
updateSlot
js
function updateSlot(returnFiber, oldFiber, newChild, lane) {
// 比较旧节点和新节点的key,不相同则返回null,相同就更新节点
const key = oldFiber !== null ? oldFiber.key : null;
//...
if (typeof newChild === 'object' && newChild !== null) {
switch (newChild.$$typeof) {
case REACT_ELEMENT_TYPE: {
if (newChild.key === key) {
// 相同key 返回更新后的节点
return updateElement(returnFiber, oldFiber, newChild, lanes)
}
else {
return null
}
}
}
}
}
}
function updateElement(returnFiber, current, element, lanes) {
if (current !== null) {
if (current.elementType === element.type) {
// 前后 组件type 没有变(div -> div)
const existing = useFiber(current, element.props);
existing.ref = coerceRef(returnFiber, current, element);
existing.return = returnFiber;
return existing;
}
// ...
}
// 前后 Fiber 的type 不一样,创建新的 Fiber返回
const created = createFiberFromElement(element, returnFiber.mode, lanes);
created.ref = coerceRef(returnFiber, current, element);
created.return = returnFiber;
return created;
}
placeChild
js
function placeChild(newFiber,lastPlacedIndex,newIndex) {
newFiber.index = newIndex; // 赋值当前新节点index
const current = newFiber.alternate; // 新节点对应复用的节点
if (current !== null) {
// 有可复用节点
const oldIndex = current.index; // 当前旧节点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;
}
}
小结
跳出 第一轮遍历 的条件:
- key 不相同,此时可能 新节点列表 还没遍历结束
- 旧列表 或者 新节点 遍历结束
遍历结束跳出循环
js
function reconcileChildrenArray() {
// ...
if (newIdx === newChildren.length) {
// 新节点列表遍历结束,删除剩余旧节点
deleteRemainingChildren(returnFiber, oldFiber);
// 返回创建的节点列表
return resultingFirstChild;
}
// 新节点没遍历完,旧节点先遍历完
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) {
resultingFirstChild = newFiber;
} else {
previousNewFiber.sibling = newFiber;
}
previousNewFiber = newFiber;
}
return resultingFirstChild;
}
}
第二轮循环
新旧节点列表都没遍历完,但节点位置有不同
js
function reconcileChildrenArray() {
// ...接上面代码
// 将剩下的 旧节点,做成 map (key或者index作为map的key,value是节点)
const existingChildren = mapRemainingChildren(returnFiber, oldFiber);
for (; newIdx < newChildren.length; newIdx++) {
// 从map中找到对应的节点 或者 创建新的节点
const newFiber = updateFromMap(
existingChildren,
returnFiber,
newIdx,
newChildren[newIdx],
lanes,
);
if (newFiber !== null) {
if (shouldTrackSideEffects) {
if (newFiber.alternate !== null) {
// 如果 map 有对应的节点,就删除map中元素
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;
}