Appearance
reconcileChildren
协调子节点,
入口
js
export function reconcileChildren(
// 当前节点的 历史FiberNode
current: Fiber | null,
// 当前节点正在处理中的 workInProgress FiberNode
workInProgress: Fiber,
// 当前节点的心 新的子节点
nextChildren: any,
renderLanes: Lanes
) {
if (current === null) {
// 如果是一个全新的组件,还没有渲染过,我们不会通过最小的副作用来更新它的子级集合。
// 相反,我们将所有子级添加到子级之前,然后渲染它。这意味着我们可以优化这个调和过程,
// 不需要跟踪副作用。
workInProgress.child = mountChildFibers(
workInProgress,
null,
nextChildren,
renderLanes
);
} else {
workInProgress.child = reconcileChildFibers(
workInProgress,
current.child,
nextChildren,
renderLanes
);
}
}
// -----------------------
// 更新阶段 shouldTrackSideEffects 为 true
export const reconcileChildFibers = ChildReconciler(true);
// 首次渲染阶段 shouldTrackSideEffects 为 false
export const mountChildFibers = ChildReconciler(false);对于入口函数其很简单,主要是根据是否存在 current 判断是否存在旧节点,然后分别进入
- 如果是首次渲染
mountChildFibers() 函数,其将 shouldTrackSideEffects 设置为 false 的协调方法,在这个处理流程中对于子节点中的可能存在的旧节点的deleteChild , 数组子节点中某一个节点后所有的兄弟节点的删除 deleteRemainingChildren , 插入节点placeChild等副作用跳过处理
- 更新阶段
reconcileChildFibers() 会执行完整的协调过程
reconcileChildFibers
这个方法是分发子节点处理方法的核心流程,其主要是根据 newChild 的类型分别对应着不同的处理
主要流程:
- 判断子节点是否是 Fragment 类型,如果是,则将子节点的 props.children 作为新的子节点。 且将 Fragment 的子节点的 return 修改成当前节点
js
// 判断是否是Fragment类型,如果是那么就第一个子组件变成 newChild.props.children
const isUnkeyedTopLevelFragment =
typeof newChild === "object" &&
newChild !== null &&
newChild.type === REACT_FRAGMENT_TYPE &&
newChild.key === null;
if (isUnkeyedTopLevelFragment) {
newChild = newChild.props.children;
}这样对于 return <><div>1</div></> 这种就可以获取到真正的子节点,而不是一个占位符节点
- 根据子元素的类型,进行不同的处理
REACT_ELEMENT_TYPE 元素类型的子节点(单个)
这个简单,直接执行 单个元素的协调过程(reconcileSingleElement),从而通过复用、创建、删除等方式生成新的 Fiber 节点树。
PortalComponent 类型的子节点
这是一个特殊的节点类型,用于将子节点渲染到指定的 DOM 节点上。
其在创建的时候就会将 $$typeof 设置为 REACT_PORTAL_TYPE,并将 key 和 element 作为 props 传递给子节点。
LazyComponent 类型的子节点
这是一个特殊的节点类型,用于实现懒加载。
其在创建的时候就会将 $$typeof 设置为 REACT_LAZY_TYPE,并将 _status 设置为 PENDING,表示懒加载的状态。
在后续的渲染过程中,会根据 _status 的值来决定是否加载对应的组件。
重点:
- 通过调用节点的
_init(newChild._payload)方法来加载对应的组件。然后再调用 reconcileChildFibers 去与真正的节点进行协调处理
reconcileSingleElement
用于处理单个 React 元素的协调过程
- 比较新老节点的 key 是否相同
- 相同的情况 : 复用老的 FiberNode
- 不同的情况 : 删除老的 FiberNode 并创建新的 FiberNode
- 判断是否是 Fragment 类型 对于 Fragment 类型的节点,需要判断是否存在真实的元素节点 并将真实元素子节点的 return 指向当前父节点
- 对于根节点中的其他子节点 进行删除
- 处理 ref ,
- 修改 newChild.return
- 创建真实的元素节点的 FiberNode
具体代码如下
js
function reconcileSingleElement(
returnFiber: Fiber,
currentFirstChild: Fiber | null,
element: ReactElement,
lanes: Lanes
): Fiber {
// 新的key
const key = element.key;
// 老的child
let child = currentFirstChild;
while (child !== null) {
// TODO: If key === null and child.key === null, then this only applies to
// the first item in the list.
// 如果新老key相同 则复用老的fiber
if (child.key === key) {
// 新的节点的类型
const elementType = element.type;
// 如果是 Fragment 类型
if (elementType === REACT_FRAGMENT_TYPE) {
// 如果老的也是 Fragment 类型
if (child.tag === Fragment) {
// 删除新的FiberNode 的兄弟节点,因为只能存在一个根节点
deleteRemainingChildren(returnFiber, child.sibling);
// 根据旧的child去克隆新的FiberNode
const existing = useFiber(child, element.props.children);
// 将clone的FiberNode 的父节点指向 函数式组件节点
existing.return = returnFiber;
if (__DEV__) {
existing._debugSource = element._source;
existing._debugOwner = element._owner;
}
return existing;
}
} else {
if (
// 新老节点的元素类型相同
child.elementType === elementType ||
// Keep this check inline so it only runs on the false path:
(__DEV__
? isCompatibleFamilyForHotReloading(child, element)
: false) ||
// Lazy types should reconcile their resolved type.
// We need to do this after the Hot Reloading check above,
// because hot reloading has different semantics than prod because
// it doesn't resuspend. So we can't let the call below suspend.
// Lazy 类型的节点判断 加载后的节点是否相同
(typeof elementType === "object" &&
elementType !== null &&
elementType.$$typeof === REACT_LAZY_TYPE &&
resolveLazy(elementType) === child.type)
) {
// 删除其他的兄弟节点
deleteRemainingChildren(returnFiber, child.sibling);
// 克隆生成新的FiberNode
const existing = useFiber(child, element.props);
existing.ref = coerceRef(returnFiber, child, element);
existing.return = returnFiber;
if (__DEV__) {
existing._debugSource = element._source;
existing._debugOwner = element._owner;
}
return existing;
}
}
// Didn't match.
deleteRemainingChildren(returnFiber, child);
break;
} else {
// 如果新老key不相同 则删除老的fiber , 标记存在 Delate 标记
deleteChild(returnFiber, child);
}
child = child.sibling;
}
// 如果新的节点是 Fragment 类型
// 1. 创建Fragment的子节点的 FiberNode
// 2. 将真实的元素节点的父节点指向 函数式组件节点
if (element.type === REACT_FRAGMENT_TYPE) {
const created = createFiberFromFragment(
element.props.children,
returnFiber.mode,
lanes,
element.key
);
created.return = returnFiber;
return created;
} else {
// 创建元素节点类型的 FiberNode
const created = createFiberFromElement(element, returnFiber.mode, lanes);
created.ref = coerceRef(returnFiber, currentFirstChild, element);
created.return = returnFiber;
return created;
}
}reconcileSinglePortal
协调 Portal 类型的子节点,其大体逻辑跟 reconcileSingleElement 类似,只是在处理节点复用的情况是需要判断 Portal 类型的节点的 containerInfo 是否相同。
js
if (
child.tag === HostPortal &&
// 绑定的目标DOM节点
child.stateNode.containerInfo === portal.containerInfo &&
child.stateNode.implementation === portal.implementation
) {
// 删除节点的兄弟节点
deleteRemainingChildren(returnFiber, child.sibling);
// 复用节点
const existing = useFiber(child, portal.children || []);
// 将clone的FiberNode 的父节点指向 函数式组件节点
existing.return = returnFiber;
return existing;
}reconcileSingleTextNode
对于子节点是 字符串 或者 数字类型 的,那么其单独进入 reconcileSingleTextNode 调和的过程,其流程也很简单,因为是单个节点,所以直接判断
- 如果旧节点第一个子节点的类型为文本类型,那就将其所有的兄弟节点添加 Deletion 标记,并通过 useFiber 复用当前 fiber 的子节点和 textContent,并指向父级 fiber
- 如果不是,那就是复用不了,进入创建新节点流程。 也是先将其所有的兄弟节点添加 Deletion 标记 ,然后通过
createFiberFromText(textContent, returnFiber.mode, lanes)创建一个新的文本节点,最后修改 return 的指向
js
function reconcileSingleTextNode(
returnFiber: Fiber,
// 旧的 子节点
currentFirstChild: Fiber | null,
// 文本类型的 内容
textContent: string,
lanes: Lanes
): Fiber {
// There's no need to check for keys on text nodes since we don't have a
// way to define them.
// 如果存在旧节点 且 其类型也是文本类型的,那么就进入复用的流程
if (currentFirstChild !== null && currentFirstChild.tag === HostText) {
// We already have an existing node so let's just update it and delete
// the rest.
// 将旧节点中当前的兄弟节点全部标记为 ChildDeletion
deleteRemainingChildren(returnFiber, currentFirstChild.sibling);
// 复用当前节点
const existing = useFiber(currentFirstChild, textContent);
existing.return = returnFiber;
return existing;
}
// The existing first child is not a text node so we need to create one
// and delete the existing ones.
// 将旧节点所有的节点都标记为 ChildDeletion
deleteRemainingChildren(returnFiber, currentFirstChild);
const created = createFiberFromText(textContent, returnFiber.mode, lanes);
created.return = returnFiber;
return created;
}placeSingleChild
在上面处理单个子节点的时候,如 REACT_ELEMENT_TYPE、REACT_PORTAL_TYPE、REACT_LAZY_TYPE 都对处理的结果通过 placeSingleChild 进行包裹处理,其主要是在更新阶段且 newFiber.alternate === null , 那么说明这个子节点是重新创建的,那么将其标记一下 Place 副作用
而对于子节点是数组类型的,其对于新生成的节点,是通过 placeChild 标记在对应单个子节点上的
js
/**
* 给 FiberNode 标记创建类型的副作用
* @param {*} newFiber
* @returns
*/
function placeSingleChild(newFiber: Fiber): Fiber {
// This is simpler for the single child case. We only need to do a
// placement for inserting new children.
// 如果没有alternate,说明是插入的新节点 那么在副作用中就标记 插入flag
if (shouldTrackSideEffects && newFiber.alternate === null) {
newFiber.flags |= Placement;
}
return newFiber;
}reconcileChildrenArray
React 协调的 Diff 算法相对于 Vue 来说就比较简单,其主要是通过三次遍历的方式去实现新老节点的复用
主要背景:
React 相信我们大部分的情况下对于数组的更新只是局部的更新,更多的只是插入一个元素或者删除一个元素,而不是更新整个数组。
例子
prev : [0,1,2,3,4,5]
next : [0,1,4,3,2,5]主要流程
- 第一次遍历:
直接按照新老数组中相同 index 的节点 key 是否相同,直到找到第一个不同的节点 index,跳出循环
结果:
js
oldFiber = FiberNode<2>;
lastPlacedIndex = 1;
newIdx = 2;
nextOldFiber = FiberNode<3>;- 通过
newIdx === newChildren.length处理新数组没有,但是旧的数组还有的情况,那么对于这种就是将旧数组剩余的节点全部删除
如
prev : [0,1,2,3,4,5]
next : [0,1,2]- 第二次遍历:
条件是: 第一次遍历后的 oldFiber 为 null 。 逻辑: 处理第一次遍历后,新数组中还有, 但是旧数组没有的情况, 这种认为新数组剩余的节点都是新增节点
js
prev: [0, 1, 2];
next: [0, 1, 2, 3, 4, 5];js
// 如果旧的节点遍历完成,但存在新的节点,那么进入第二次遍历,对后面的新节点进行 插入操作
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], lanes);
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;
}- 第三次遍历
当触发这种情况,那么说明新数组和旧数组都有,但是位置发生了变化。
那么其主要的逻辑是:
- 先将旧数组剩余的节点按照 Key 或者 Index 转成 map 结构
- 然后从上次跳出的下标继续遍历新数组,
- 判断新数组中的节点是否在 map 中存在,如果存在则复用,否则创建新的 FiberNode 节点
- 对于复用节点:1. 则调整新节点的兄弟节点的指向,
- 对于复用节点:2. 并标记节点是否存在 place 副作用
- 删除剩余没有被复用的旧节点
js
// Add all children to a key map for quick lookups.
// 将剩余的旧节点按照 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],
lanes
);
// 如果不为null,说明可以复用,那么调整新节点的兄弟节点的指向,并标记节点是否存在 place 副作用
if (newFiber !== null) {
if (shouldTrackSideEffects) {
// 如果存在旧的节点,那么说明这个旧的节点被复用了,就将其从 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));
}1. updateSlot
对于 key 相同的新旧节点,其会经过 updateSlot 去判断节点是否真正可以复用,并对于可以复用的节点进行对应的处理,如
2. 文本类型 - updateTextNode
更新或者创建 文本类型的 FiberNode
3. 元素类型的节点 - updateElement
4. PortalComponent - updatePortal
这个和单节点的 reconcileSinglePortal 的区别是什么?
在 reconcileSinglePortal 中 需要根据 Key 判断可以复用,且还需要处理旧节点中的兄弟节点,但是对于 updateSlot 来说,其 key 肯定相同,且不需要关心旧节点的兄弟节点,所以 updatePortal 的核心就是比较旧节点的 tag 类型且 conitainerInfo、implementation 都要相同
js
/**
* 1. 比较新老节点的 tag 类型 相同 且 conitainerInfo、implementation 都要相同
* - 相同的情况 : 调用 useFiber 复用老的 FiberNode
* - 不同的情况 : 删除老的 FiberNode 并创建新的 FiberNode
* 2. 不同的情况: 删除老的 FiberNode 并创建新的 FiberNode
* @param {*} returnFiber
* @param {*} current
* @param {*} portal
* @param {*} lanes
* @returns
*/
function updatePortal(
returnFiber: Fiber,
current: Fiber | null,
portal: ReactPortal,
lanes: Lanes
): Fiber {
// 判断除了key相同外,其他的条件是否符合复用流程
// 节点类型、 挂载对象 、
if (
current === null ||
current.tag !== HostPortal ||
current.stateNode.containerInfo !== portal.containerInfo ||
current.stateNode.implementation !== portal.implementation
) {
// Insert
const created = createFiberFromPortal(portal, returnFiber.mode, lanes);
created.return = returnFiber;
return created;
} else {
// Update
const existing = useFiber(current, portal.children || []);
existing.return = returnFiber;
return existing;
}
}5. REACT_LAZY_TYPE
对于 懒加载类型的 节点,其主要是通过 updateSlot去判断懒加载后的 init(payload) 节点是否可以复用
placeChild
尽量采用向后插入节点的方式,去判断节点是否存在 Place 类型的副作用
prev : [0,1,2,3,4,5]
next : [0,1,4,3,2,5]这个例子中在第一次循环遍历后 prev = [2,3,4,5],next = [4,3,2,5] , 并且第二步和第三步也不成立,那么在第三次遍历的流程中,需要按照新数组的顺序去处理旧数组的节点, 那么对于上述其最大复用节点数组就是 [4,5] , 其遍历到 3、2 的时候虽然可以复用,但是其仍然需要进行插入操作。
所以在 placeChild 方法中 lastPlacedIndex 取得是复用的旧节点下标和 新节点下标的最大值,这样确保在新节点数组中移动到前面的节点也需要通过插入操作来更新位置
js
/**
* 尽量采用向后插入节点的方式,去判断节点是否存在 Place 类型的副作用
* 其核心是通过 lastPlacedIndex 来最大化的复用老的不需要处理的节点,
* 并根据 lastPlacedIndex 和 newIndex 来判断进行**移动操作**
* 不存在 current 进行 Place 操作
* @param {*} newFiber
* @param {*} lastPlacedIndex
* @param {*} newIndex
* @returns
*/
function placeChild(
newFiber: Fiber,
lastPlacedIndex: number,
newIndex: number
): number {
// 将新节点的下标设置为新的下标
newFiber.index = newIndex;
if (!shouldTrackSideEffects) {
// During hydration, the useId algorithm needs to know which fibers are
// part of a list of children (arrays, iterators).
newFiber.flags |= Forked;
return lastPlacedIndex;
}
// 旧节点
const current = newFiber.alternate;
if (current !== null) {
// 旧节点原来的下标
const oldIndex = current.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;
}
}