前言:
在 React源码解析之FunctionComponent(中) 中,讲到了reconcileSingleElement()
和reconcileSingleTextNode()
:
function reconcileChildFibers() {
if (isObject) {
switch (newChild.$$typeof) {
// ReactElement节点
case REACT_ELEMENT_TYPE:
return placeSingleChild(
reconcileSingleElement());
}
}
//文本节点
if (typeof newChild === 'string' || typeof newChild === 'number') {
return placeSingleChild(
reconcileSingleTextNode());
}
//数组节点,也是本文要讲的
if (isArray(newChild)) {
return reconcileChildrenArray();
}
}
接下来,我们讲reconcileChildrenArray()
是如何更新数组节点的
一、reconcileChildrenArray
作用:
更新数组节点
源码:
function reconcileChildrenArray(
returnFiber: Fiber,
currentFirstChild: Fiber | null,
//待更新的数组节点
newChildren: Array<*>,
expirationTime: ExpirationTime,
): Fiber | null {
// This algorithm can't optimize by searching from both 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.
//删除了 dev 代码
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++) {
// 当要更新的节点的 index 大于 newIndex 时,
// 说明它不在所期盼的位置上,则需要“认真处理”oldFiber
if (oldFiber.index > newIdx) {
nextOldFiber = oldFiber;
oldFiber = null;
}
//否则,则处理该节点的下一个兄弟节点
else {
nextOldFiber = oldFiber.sibling;
}
//复用或新建节点
const newFiber = updateSlot(
//当前节点的父节点
returnFiber,
//旧节点
oldFiber,
//待更新的新节点
newChildren[newIdx],
expirationTime,
);
//说明key 不相同,节点不能复用,此时就跳出循环
//如果不跳出循环,说明可以是相同的
//也就是说当跳出循环的时候,我们可以知道截至目前,复用节点的个数,和不可复用节点的 index,
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 === null) {
oldFiber = nextOldFiber;
}
break;
}
//初次渲染的情况下
if (shouldTrackSideEffects) {
//newFiber.alternate表示并没有复用 oldFiber 来赋值,而是 return 了新的 fiber
//所以要删除存在的 旧的fiber
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);
}
}
//将 newFiber 节点挂载到 DOM 树上,返回最新移动的 index
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;
}
//跳出循环后
//index=length,说明截止到最后,所有节点都是可以复用的
//故可以删除老节点
if (newIdx === newChildren.length) {
// 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 (; 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;
}
// Add all children to a key map for quick lookups.
//数组可能存在顺序的变化,oldfiber和 newfiber 还有可以复用的 fiber 节点
const existingChildren = mapRemainingChildren(returnFiber, oldFiber);
// Keep scanning and use the map to restore deleted items as moves.
//继续遍历剩下的 new 节点
for (; newIdx < newChildren.length; newIdx++) {
const newFiber = updateFromMap(
existingChildren,
returnFiber,
newIdx,
newChildren[newIdx],
expirationTime,
);
if (newFiber !== null) {
if (shouldTrackSideEffects) {
//不为 null 说明 fiber 节点已经被复用了,所以可以从 Map 中删除
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;
}
解析:
(1) 循环数组节点,在循环中主要做了如下几点:
① 将 oldFiber 的 index与 newIdx 进行比较,
如果 oldFIber.index 大,则将 oldFiber 赋值给 nextOldFiber(表示需要处理);
如果 newIdx 大,则将 oldFiber.sibling 赋值给 nextOldFiber
② 执行updateSlot()
,复用或新建节点,返回的结果赋值给newFiber
③ 如果newFiber
的值为空的话,说明该节点不能复用,则跳出循环(break
)
④ 如果是第一次渲染(即shouldTrackSideEffects
为 true),并且 newFiber 没有要复用的 oldFiber 的话,则删除该 fiber 下的所有子节点
关于deleteChild
的讲解,请看:React源码解析之FunctionComponent(中)
⑤ 执行placeChild()
,将 newFiber 节点挂载到 DOM 树上,并判断更新后是否移动过,如果移动,则需要重新挂载,返回最新移动的 index,并赋值给lastPlacedIndex
⑥ previousNewFiber
那段,意思是为数组里的每一个 fiber 节点设置 sibling 属性,即它旁边的 fiber(index+1)
(2) 跳出循环后,如果newIdx
和更新的数组长度相等,则表示所有节点都是可以复用的,那么就执行deleteRemainingChildren()
,删除旧节点
(3) 如果旧节点都已经被复用完了,但是仍有部分新节点需要被创建的话,则循环剩余数组的长度,并依次创建新节点(部分代码与上面重复,不再赘述)
(4) 如果仍有旧节点剩余的话,则执行mapRemainingChildren()
,将这些旧节点用 Map 结构集合起来,看有没有方便 newFiber 复用的节点
(5) 继续遍历剩下的 new 节点
① 执行updateFromMap()
,查找有没有 key/index 相同的点,方便复用
② if (newFiber !== null)
的部分逻辑与上面相同,不再赘述
(6) 如果是第一次渲染的话,则删除没有复用的节点
(7) 最终返回 更新后的数组的第一个节点(根据它的 silbing 属性,可找到其他节点)
后面的部分是针对reconcileChildrenArray()
出现的一些函数的补充
二、updateSlot
作用:
复用或新建节点
源码:
//复用或新建节点
//key 相同的情况下,进行节点复用;
//key 不同的情况下,无法复用
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;
//文本节点是没有 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.
//如果老节点有 key 的话,说明是从 ReactElement 节点转变为文本节点了
// 这样也没关系,可以不间断更新
if (key !== null) {
return null;
}
//执行updateTextNode,对文本节点进行更新
return updateTextNode(
returnFiber,
oldFiber,
'' + newChild,
expirationTime,
);
}
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;
}
解析:
(1) 如果是文本节点的话,是不能根据 key 去判断是否复用的,注意下
(2) 如果是其他节点类型的话,则执行相应的函数,来进行节点更新(key 相同则复用)
三、placeChild
作用:
将 newFiber 节点挂载到 DOM 树上,并判断更新后是否移动过,如果移动,则需要重新挂载,返回最新移动的 index
源码:
function placeChild(
newFiber: Fiber,
lastPlacedIndex: number,
newIndex: number,
): number {
newFiber.index = newIndex;
if (!shouldTrackSideEffects) {
// Noop.
return lastPlacedIndex;
}
const current = newFiber.alternate;
if (current !== null) {
const oldIndex = current.index;
//移动了的节点
if (oldIndex < lastPlacedIndex) {
// This is a move.
//因为是移动的节点,所以要重新挂载到 DOM 上
newFiber.effectTag = Placement;
return lastPlacedIndex;
} else {
//没有移动
// This item can stay in place.
return oldIndex;
}
}
//current 为 null 说明该节点没有被渲染过
//所以是新插入的节点
else {
// This is an insertion.
newFiber.effectTag = Placement;
return lastPlacedIndex;
}
}
解析:
(1) 如果不是初次渲染的话(shouldTrackSideEffects 为 true),无需更新shouldTrackSideEffects
(2) newFiber.alternate
有值的话,说明是由旧节点更新来的,那么就需要比较oldIndex
和lastPlacedIndex
,有移动过的话,则返回lastPlacedIndex
,否则返回oldIndex
(3) newFiber.alternate
没有值的话,说明不是由旧节点更新来的,而是新插入的节点,返回lastPlacedIndex
四、mapRemainingChildren
作用:
将旧节点用 Map 结构集合起来,方便 newFiber 复用
源码:
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.
const existingChildren: Map<string | number, Fiber> = new Map();
let existingChild = currentFirstChild;
//遍历剩下的节点,获取其 key
while (existingChild !== null) {
if (existingChild.key !== null) {
existingChildren.set(existingChild.key, existingChild);
} else {
existingChildren.set(existingChild.index, existingChild);
}
existingChild = existingChild.sibling;
}
//创建了一个 Map 对象,以便找到key 相同的节点,方便复用
return existingChildren;
}
解析:
利用 Map 结构,遍历剩下的 oldFiber,以key-value
的形式,将这些旧节点存到 Map 中,如果没有key
的话,则说明是文本节点,则以index-value
的形式存储,最终返回这个 Map 对象
五、updateFromMap
作用:
在 Map 对象中查找有没有 key/index 相同的 fiber 节点,方便复用
源码:
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.
//如果是文本节点的话,会从 Map 对象中寻找是否有相同的 index(为什么不是key?因为文本节点没有 key 属性)
const matchedFiber = existingChildren.get(newIdx) || null;
return updateTextNode(
returnFiber,
matchedFiber,
'' + newChild,
expirationTime,
);
}
if (typeof newChild === 'object' && newChild !== null) {
switch (newChild.$$typeof) {
case REACT_ELEMENT_TYPE: {
//updateSlot()是根据 key 是否相同来判断,这边是根据 Map 中是否有key/index 来判断
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;
}
解析:
和二、updateSlot
的内容差不多,不再赘述
关于FunctionComponent
的更新讲解就先到这里结束了
GitHub:
ReactChildFiber
(完)
网友评论