1 )概述
2 )源码
在 reconcileChildFibers 函数中调用的 reconcileChildrenArray
定位到 packages/react-reconciler/src/ReactChildFiber.js#L732
先看下 reconcileChildrenArray 这个API
新老children的对比过程,以及判断节点是否可复用的过程,这个过程会涉及到react的一个算法
尽量减少 数组的遍历次数,达到复用节点的过程
// 对于 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;
}
updateSlot
// 这个方法对比,新老的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
function 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
function 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
function 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
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;
}