reconcileChildIterator 和 reconcileChildrenArray
1 )概述
- 在react更新某一个节点的时候,要根据这个节点,它的类型去获取它的children
- 比如说如果是 Function Component,它要调用这个 component 计算出它的return的属性
- return的属性可能是一个数组,可能是单个的 ReactElement,可能是 number, string 这些类型
- 要根据这些不同的类型去进行一些特殊的处理,然后得到它的children,和对应的 fiber 对象
- 这样的话,又可以继续往下去迭代,迭代到最终的一个节点
- 在经过这个过程,就可以把一个 fiber 树的一侧的子树进行完整的遍历和构建的过程
- 在这个过程当中,有一个 children 的类型比较特殊,那就是数组
- 在我们使用数组作为children返回的时候,都会看到这样的一个提醒
- 那就是我们每一个节点上面必须要有一个key属性
- 如果这个key属性没有,那 react 就会给我们一个提醒
- 我们必须要对数组中的每一个节点使用一个唯一的key来标识它
- 这个key和数组调和有特定的关系,主要关注:
- A. key的作用
- B. 对比数组 children 是否可复用的一个过程
- C. generator 和 array遍历的区别
2 )源码
在 reconcileChildFibers 函数中调用的 reconcileChildrenArray
定位到 packages/react-reconciler/src/ReactChildFiber.js#L732
先看下 reconcileChildrenArray 这个API
新老children的对比过程,以及判断节点是否可复用的过程,这个过程会涉及到react的一个算法
尽量减少 数组的遍历次数,达到复用节点的过程
js
// 对于 newChildren 的遍历本质上是 O(n)
// 第一次遍历通过 newIdx++ 的方式,后面 特殊情况的判断来加速性能提升
// 只有在最后一个 for 循环中进行完整遍历,完成后创建节点
// 这里有很多判断一个节点是否可复用,都是通过 key 是否存在来判断的
// 如果不服用并且创建新的 fiber 节点,删除老的 fiber 节点 会导致内存申请和内存回收频繁
// 整体对性能存在影响,因为过于频繁的内存回收会导致内存抖动的问题
function reconcileChildrenArray(
returnFiber: Fiber,
currentFirstChild: Fiber | null,
newChildren: Array<*>,
expirationTime: ExpirationTime,
): Fiber | null {
// This algorithm can't optimize by searching from boths ends since we
// don't have backpointers on fibers. I'm trying to see how far we can get
// with that model. If it ends up not being worth the tradeoffs, we can
// add it later.
// Even with a two ended optimization, we'd want to optimize for the case
// where there are few changes and brute force the comparison instead of
// going for the Map. It'd like to explore hitting that path first in
// forward-only mode and only go for the Map once we notice that we need
// lots of look ahead. This doesn't handle reversal as well as two ended
// search but that's unusual. Besides, for the two ended optimization to
// work on Iterables, we'd need to copy the whole set.
// In this first iteration, we'll just live with hitting the bad case
// (adding everything to a Map) in for every insert/move.
// If you change this code, also update reconcileChildrenIterator() which
// uses the same algorithm.
if (__DEV__) {
// First, validate keys.
let knownKeys = null;
for (let i = 0; i < newChildren.length; i++) {
const child = newChildren[i];
knownKeys = warnOnInvalidKey(child, knownKeys);
}
}
// 声明一堆的变量
let resultingFirstChild: Fiber | null = null;
let previousNewFiber: Fiber | null = null;
let oldFiber = currentFirstChild; // 上一次渲染的过程中,渲染完成后,当前节点的第一个child节点
let lastPlacedIndex = 0;
let newIdx = 0;
let nextOldFiber = null;
// 这个for循环以相同的顺序,分别遍历新老的children对应的节点,判断它的key是否相同
// 如果不相同,则跳出循环,到这个节点为止
// 在遍历新老数组的时候,找到第一个不能复用的节点,这时候就会跳出循环
for (; oldFiber !== null && newIdx < newChildren.length; newIdx++) {
// 对于react渲染整个数组的过程中,会在每个 Fiber 节点上面 设置一个index属性 就是 这个节点在children里的位置
// 老的children的Fiber的index > newIdx 说明它们的位置不匹配,则直接赋值给 nextOldFiber
if (oldFiber.index > newIdx) {
nextOldFiber = oldFiber;
oldFiber = null;
} else {
// 正常情况下,取当前节点的下一个节点
nextOldFiber = oldFiber.sibling;
}
const newFiber = updateSlot(
returnFiber,
oldFiber,
newChildren[newIdx],
expirationTime,
);
// 为 null 代表 这个节点不能复用 跳出 for循环
if (newFiber === null) {
// TODO: This breaks on empty slots like null children. That's
// unfortunate because it triggers the slow path all the time. We need
// a better way to communicate whether this was a miss or null,
// boolean, undefined, etc.
// 如果oldFiber 不存在,则处理成下一个节点
if (oldFiber === null) {
oldFiber = nextOldFiber;
}
break;
}
// 存在 newFiber 并且 shouldTrackSideEffects
if (shouldTrackSideEffects) {
// newFiber.alternate 不存在,说明它没有复用 oldFiber 来产生一个节点,而是直接 return 了一个新的Fiber
// 如果重新复用的 oldFiber, 那 newFiber.alternate 应该存在的,这个节点至少经过一次渲染,是有 current 和 workInProgress 的存在的
// 没有复用之前的节点,则说明 老的节点 失效的状况,则删除之
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);
// 每次循环都会给 previousNewFiber 赋值
// 如果没有被赋值,代表是新节点
if (previousNewFiber === null) {
// TODO: Move out of the loop. This only happens for the first run.
resultingFirstChild = newFiber; // 这里
} else {
// TODO: Defer siblings if we're not at the right index for this slot.
// I.e. if we had null values before, then we want to defer this
// for each null value. However, we also don't want to call updateSlot
// with the previous one.
previousNewFiber.sibling = newFiber; // 如果之前节点已经存在, newFiber 是之前节点previousNewFiber的兄弟节点
}
previousNewFiber = newFiber; // 这里进行赋值
oldFiber = nextOldFiber; // 接着下一轮
}
// 跳出循环后,当两者相等,新数组的children 全部创建fiber对象了, 新数组已经操作完成了
if (newIdx === newChildren.length) {
// We've reached the end of the new children. We can delete the rest.
deleteRemainingChildren(returnFiber, oldFiber); // 对于老数组情况,存在 oldFiber,删除剩下的节点
return resultingFirstChild; // 返回第一个节点,第一个节点才是 return fiber 的 child 属性所指向的节点,剩下的后续节点都是通过 .sibling 来指向下去的
}
// oldFiber 为 null 时,说明老节点已经被遍历完了
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],
expirationTime,
);
if (!newFiber) {
continue;
}
// 同样对这些节点进行 placeChild 操作
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; // 存在,则处理 sibling 指向
}
previousNewFiber = newFiber;
}
return resultingFirstChild;
}
// Add all children to a key map for quick lookups.
// 剩下的情况是数组可能存在顺序的变化,oldFiber 可能还有一些兄弟节点,newChildren 还有几个没有被创建
// 从 oldFiber 剩下的节点中找到 newChildren 可以复用的 Fiber 节点
// 通过 mapRemainingChildren 来创建一个map
const existingChildren = mapRemainingChildren(returnFiber, 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],
expirationTime,
);
// 存在 newFiber
if (newFiber) {
if (shouldTrackSideEffects) {
// 存在 current,说明已经被复用了
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 里面的删除该匹配的节点
existingChildren.delete(
newFiber.key === null ? newIdx : newFiber.key,
);
}
}
// 没有被复用, 执行 placeChild
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 中遗留的 fiber 对象执行 删除,因为这些fiber对象没有被复用
existingChildren.forEach(child => deleteChild(returnFiber, child));
}
return resultingFirstChild;
}
- 下面是上面用到的几个API
-
进入
updateSlot
js// 这个方法对比,新老的key是否相同,来查看它是否可以复用 老的fiber节点 function updateSlot( returnFiber: Fiber, oldFiber: Fiber | null, newChild: any, expirationTime: ExpirationTime, ): Fiber | null { // Update the fiber if the keys match, otherwise return null. // oldFiber 存在的情况下,获取它的key const key = oldFiber !== null ? oldFiber.key : null; // 文本节点,没有key if (typeof newChild === 'string' || typeof newChild === 'number') { // Text nodes don't have keys. If the previous node is implicitly keyed // we can continue to replace it without aborting even if it is not a text // node. if (key !== null) { return null; } // 老的节点存在 key return updateTextNode( returnFiber, oldFiber, '' + newChild, expirationTime, ); } // 对象类型,基于 $$typeof 来判断 if (typeof newChild === 'object' && newChild !== null) { switch (newChild.$$typeof) { case REACT_ELEMENT_TYPE: { // 只有在前后的key相同的情况下,才会复用节点 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; }
-
进入
placeChild
jsfunction placeChild( newFiber: Fiber, lastPlacedIndex: number, newIndex: number, ): number { // 同步 index newFiber.index = newIndex; if (!shouldTrackSideEffects) { // Noop. return lastPlacedIndex; } // 获取 current const current = newFiber.alternate; // 存在 current if (current !== null) { const oldIndex = current.index; if (oldIndex < lastPlacedIndex) { // This is a move. newFiber.effectTag = Placement; // 代表这个节点要被挂载到 dom上面 顺序变化了,说明这个节点被移动了,进行 dom 操作,tag 就是 Placement return lastPlacedIndex; } else { // This item can stay in place. return oldIndex; // 存在于原来的位置 } } else { // current 为 null 说明节点没有被渲染过,它是一个插入的节点 // 插入的节点也是需要使用 Placement 同样进行dom操作 // 就是这个节点要根据某个顺序插入到 dom节点的后面 // This is an insertion. newFiber.effectTag = Placement; return lastPlacedIndex; } }
-
进入
mapRemainingChildren
jsfunction mapRemainingChildren( returnFiber: Fiber, currentFirstChild: Fiber, ): Map<string | number, Fiber> { // 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 // instead. // 通过 Map 对象找到 key相同的节点,判断是否可以复用 const existingChildren: Map<string | number, Fiber> = new Map(); let existingChild = currentFirstChild; // 遍历剩下的节点,获取其key while (existingChild !== null) { if (existingChild.key !== null) { // set key value existingChildren.set(existingChild.key, existingChild); } else { // key 不存在,使用 index existingChildren.set(existingChild.index, existingChild); } existingChild = existingChild.sibling; } return existingChildren; }
-
进入
updateFromMap
jsfunction updateFromMap( existingChildren: Map<string | number, Fiber>, returnFiber: Fiber, newIdx: number, newChild: any, expirationTime: ExpirationTime, ): Fiber | null { // 匹配 文本节点 if (typeof newChild === 'string' || typeof newChild === 'number') { // Text nodes don't have keys, so we neither have to check the old nor // new node for the key. If both are text nodes, they match. const matchedFiber = existingChildren.get(newIdx) || null; // 通过 newIdx 来查找,不管是否找到 // 返回一个text node return updateTextNode( returnFiber, matchedFiber, '' + newChild, expirationTime, ); } // 这里和 updateSlot类似 if (typeof newChild === 'object' && newChild !== null) { switch (newChild.$$typeof) { case REACT_ELEMENT_TYPE: { const matchedFiber = existingChildren.get( newChild.key === null ? newIdx : newChild.key, ) || null; if (newChild.type === REACT_FRAGMENT_TYPE) { return updateFragment( returnFiber, matchedFiber, newChild.props.children, expirationTime, newChild.key, ); } return updateElement( returnFiber, matchedFiber, newChild, expirationTime, ); } case REACT_PORTAL_TYPE: { const matchedFiber = existingChildren.get( newChild.key === null ? newIdx : newChild.key, ) || null; return updatePortal( returnFiber, matchedFiber, newChild, expirationTime, ); } } if (isArray(newChild) || getIteratorFn(newChild)) { const matchedFiber = existingChildren.get(newIdx) || null; return updateFragment( returnFiber, matchedFiber, newChild, expirationTime, null, ); } throwOnInvalidObjectType(returnFiber, newChild); } if (__DEV__) { if (typeof newChild === 'function') { warnOnFunctionType(); } } return null; }
-
在 reconcileChildFibers 函数中调用的 reconcileChildrenIterator
定位到 packages/react-reconciler/src/ReactChildFiber.js#L891
js
function reconcileChildrenIterator(
returnFiber: Fiber,
currentFirstChild: Fiber | null,
newChildrenIterable: Iterable<*>,
expirationTime: ExpirationTime,
): Fiber | null {
// This is the same implementation as reconcileChildrenArray(),
// but using the iterator instead.
// 获取 iteratorFn
const iteratorFn = getIteratorFn(newChildrenIterable);
invariant(
typeof iteratorFn === 'function',
'An object is not an iterable. This error is likely caused by a bug in ' +
'React. Please file an issue.',
);
if (__DEV__) {
// We don't support rendering Generators because it's a mutation.
// See https://github.com/facebook/react/issues/12995
if (
typeof Symbol === 'function' &&
// $FlowFixMe Flow doesn't know about toStringTag
newChildrenIterable[Symbol.toStringTag] === 'Generator'
) {
warning(
didWarnAboutGenerators,
'Using Generators as children is unsupported and will likely yield ' +
'unexpected results because enumerating a generator mutates it. ' +
'You may convert it to an array with `Array.from()` or the ' +
'`[...spread]` operator before rendering. Keep in mind ' +
'you might need to polyfill these features for older browsers.',
);
didWarnAboutGenerators = true;
}
// Warn about using Maps as children
if ((newChildrenIterable: any).entries === iteratorFn) {
warning(
didWarnAboutMaps,
'Using Maps as children is unsupported and will likely yield ' +
'unexpected results. Convert it to a sequence/iterable of keyed ' +
'ReactElements instead.',
);
didWarnAboutMaps = true;
}
// First, validate keys.
// We'll get a different iterator later for the main pass.
// 获取 newChildren,这里 newChildrenIterable 是具有 迭代特性的 children
const newChildren = iteratorFn.call(newChildrenIterable);
if (newChildren) {
let knownKeys = null;
let step = newChildren.next();
for (; !step.done; step = newChildren.next()) {
const child = step.value;
knownKeys = warnOnInvalidKey(child, knownKeys);
}
}
}
const newChildren = iteratorFn.call(newChildrenIterable);
invariant(newChildren != null, 'An iterable object provided no iterator.');
// 声明很多变量
let resultingFirstChild: Fiber | null = null;
let previousNewFiber: Fiber | null = null;
let oldFiber = currentFirstChild;
let lastPlacedIndex = 0;
let newIdx = 0;
let nextOldFiber = null;
// 通过 next 向下获取新节点
let step = newChildren.next();
// 这个 for 循环的判断是基于迭代器的,当遍历结束 .done 返回的是 false
// step 都是 next,在 Fiber 中的key 如果不存在,使用 index
// 这里也要模拟一个 index 出来
for (
;
oldFiber !== null && !step.done;
newIdx++, step = newChildren.next()
) {
if (oldFiber.index > newIdx) {
nextOldFiber = oldFiber;
oldFiber = null;
} else {
nextOldFiber = oldFiber.sibling;
}
const newFiber = updateSlot(
returnFiber,
oldFiber,
step.value,
expirationTime,
);
if (newFiber === null) {
// TODO: This breaks on empty slots like null children. That's
// unfortunate because it triggers the slow path all the time. We need
// a better way to communicate whether this was a miss or null,
// boolean, undefined, etc.
if (!oldFiber) {
oldFiber = nextOldFiber;
}
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) {
// TODO: Move out of the loop. This only happens for the first run.
resultingFirstChild = newFiber;
} else {
// TODO: Defer siblings if we're not at the right index for this slot.
// I.e. if we had null values before, then we want to defer this
// for each null value. However, we also don't want to call updateSlot
// with the previous one.
previousNewFiber.sibling = newFiber;
}
previousNewFiber = newFiber;
oldFiber = nextOldFiber;
}
if (step.done) {
// We've reached the end of the new children. We can delete the rest.
deleteRemainingChildren(returnFiber, oldFiber);
return resultingFirstChild;
}
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 (; !step.done; newIdx++, step = newChildren.next()) {
const newFiber = createChild(returnFiber, step.value, 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;
}
// Add all children to a key map for quick lookups.
const existingChildren = mapRemainingChildren(returnFiber, oldFiber);
// Keep scanning and use the map to restore deleted items as moves.
for (; !step.done; newIdx++, step = newChildren.next()) {
const newFiber = updateFromMap(
existingChildren,
returnFiber,
newIdx,
step.value,
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.
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;
}
- 这个 reconcileChildIterator 方法和上面的 reconcileChildrenArray 基本一致
- 就是判断条件不同,不再赘述
- 上述两个 API 是 对可遍历的 children 调和的一个过程
- 目的是尽量的复用可复用的 fiber 节点
- 减少对象声明和内存回收的过程