多个子节点
什么情况下newChild会是Array
- children是一个数组
function Com1() {
return array.map(i => <span key={i}>{i}</span>);
}
- child的父级为Fragment
function Com() {
return (
<>
<span>1</span>
<span>2</span>
<span>3</span>
</>
);
}
React是如何处理多个节点的?
- 逐个对比两个数组,尝试通过index的顺序以及对比key来复用fiber节点
- 调用updateSlot对比index相同的child的key是否相同,如果是,返回该对象,如果不是,返回null
- 找道第一个不相等的位子,就跳出循环
- 如果newChildren遍历完毕,就删除旧链表的剩余节点
- 如果旧的遍历完毕,就插入newChildren剩余节点
- 如果以上都不满足,就创建一个map,保存所有没有匹配到的节点,然后新的数组根据key从这个 map 里面查找,如果有则复用,没有则新建。
- 在遍历过程中会调用placeChild方法,用于判断节点的 effects 是否该设置为 Placement
function reconcileChildrenArray(
returnFiber: Fiber,
currentFirstChild: Fiber | null,
newChildren: Array<*>,
expirationTime: ExpirationTime,
): 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;
}
//// 如果可以复用就返回复用的fiber节点 否则返回null
// 根据key来判断是否能够复用
const newFiber = updateSlot(
returnFiber,
oldFiber,
newChildren[newIdx],
expirationTime,
);
//newFiber为null,跳出循环
if (newFiber === null) {
if (oldFiber === null) {
oldFiber = nextOldFiber;
}
break;
}
if (shouldTrackSideEffects) {
//返回的newFiber是新创建的,没有复用
if (oldFiber && newFiber.alternate === null) {
//删除oldFiber
deleteChild(returnFiber, oldFiber);
}
}
// 判断节点的 effects 是否为 Placement
lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);
// 将复用的fiber节点链接成一个链表
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;
}
//遍历newChildren完毕,删除旧链表剩余节点
if (newIdx === newChildren.length) {
// We've reached the end of the new children. We can delete the rest.
deleteRemainingChildren(returnFiber, oldFiber);
return resultingFirstChild;
}
// oldFiber不存在,表明旧的children已经遍历完毕,但是newChildren存在,说明是新增,剩余的新数组的项就可以作为新的项直接插入进去了。
if (oldFiber === null) {
// 遍历newChildren 创建新的fiber,通过sibling关联
for (; newIdx < newChildren.length; newIdx++) {
const newFiber = createChild(
returnFiber,
newChildren[newIdx],
expirationTime,
);
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;
}
// 创建一个map,保存所有没有匹配到的节点,然后新的数组根据key从这个 map 里面查找,如果有则复用,没有则新建。
const existingChildren = mapRemainingChildren(returnFiber, oldFiber);
// Keep scanning and use the map to restore deleted items as moves.
//从上面挑出for循环的索引开始继续向后遍历
for (; newIdx < newChildren.length; newIdx++) {
// 从map中取出和newChild的key值相同的fiber,然后创建fiber,更新属性,
const newFiber = updateFromMap(
existingChildren,
returnFiber,
newIdx,
newChildren[newIdx],
expirationTime,
);
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.
// 存在则删除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;
}
updateSlot
对比新老的 key 是否相同,来查看是否可以复用老的节点。
判定规则:
- Text节点:复用(key都为undefined)
- Element节点:判断key是否相同以及type是否一致
- 数组节点:当初顶层Fragment来处理(key都为undefined)
判定结果:
- 如果符合规则则返回fiber节点,不符合返回null
elementType不匹配的话返回的fiber是新的而不是复用,也没有alternate绑定的关系
function updateSlot( returnFiber: Fiber, oldFiber: Fiber | null, newChild: any, expirationTime: ExpirationTime, ): Fiber | null { // Update the fiber if the keys match, otherwise return null. const key = oldFiber !== null ? oldFiber.key : null; if (typeof newChild === 'string' || typeof newChild === 'number') { // 对于新的节点如果是 string 或者 number,那么都是没有 key 的, // 所有如果老的节点有 key 的话,就不能复用,直接返回 null。 if (key !== null) { return null; } // 老的节点 key 为 null 的话,代表老的节点是文本节点,就可以复用 return updateTextNode( returnFiber, oldFiber, '' + newChild, expirationTime, ); } if (typeof newChild === 'object' && newChild !== null) { switch (newChild.$$typeof) { case REACT_ELEMENT_TYPE: { if (newChild.key === key) { if (newChild.type === REACT_FRAGMENT_TYPE) { return updateFragment( returnFiber, oldFiber, newChild.props.children, expirationTime, key, ); } return updateElement( returnFiber, oldFiber, newChild, expirationTime, ); } else { return null; } } case REACT_PORTAL_TYPE: { if (newChild.key === key) { return updatePortal( returnFiber, oldFiber, newChild, expirationTime, ); } else { return null; } } } if (isArray(newChild) || getIteratorFn(newChild)) { if (key !== null) { return null; } return updateFragment( returnFiber, oldFiber, newChild, expirationTime, null, ); } throwOnInvalidObjectType(returnFiber, newChild); } if (__DEV__) { if (typeof newChild === 'function') { warnOnFunctionType(); } } return null; }
mapRemainingChildren
function mapRemainingChildren(returnFiber, currentFirstChild) {
// Add the remaining children to a temporary map so that we can find them by
// keys quickly. Implicit (null) keys get added to this set with their index
var existingChildren = new Map();
var existingChild = currentFirstChild;
while (existingChild !== null) {
if (existingChild.key !== null) {
existingChildren.set(existingChild.key, existingChild);
} else {
existingChildren.set(existingChild.index, existingChild);
}
existingChild = existingChild.sibling;
}
return existingChildren;
}
判断数组元素是否应该被移除的逻辑
这里尽量采用向后插入节点的方式,而不是向前插入
举个例子:
prev:[0, 1, 2, 3, 4]
next: [0, 4, 3, 1, 2]
- lastPlacedIndex = 0; oldIndex = 0; return 0;
- lastPlacedIndex = 0; oldIndex = 4; oldIndex>=lastPlacedIndex; return 4;
- lastPlacedIndex = 4; oldIndex = 3; <; return lastPlacedIndex(4); 最终得出effect列表:0 4 不需要移动,其他的都需要移动
function placeChild(
newFiber: Fiber,
lastPlacedIndex: number,
newIndex: number,
): number {
newFiber.index = newIndex; // index按照element的顺序
if (!shouldTrackSideEffects) {
// Noop.
return lastPlacedIndex; // 这样不是永远都是0吗
}
const current = newFiber.alternate;
if (current !== null) {
const oldIndex = current.index;
if (oldIndex < lastPlacedIndex) {
// This is a move.
newFiber.effectTag = Placement;
return lastPlacedIndex;
} else {
// >=
// This item can stay in place.
return oldIndex;
}
} else {
// This is an insertion.
newFiber.effectTag = Placement;
return lastPlacedIndex;
}
}