上一篇讲到,函数组件在 beginWork 阶段会进入 updateFunctionComponent,然后通过 renderWithHooks 执行组件函数。
例如:
function App() {
return (
<div>
<span>Hello React</span>
</div>
)
}
renderWithHooks 执行完 App() 之后,得到的不是 DOM,也不是 Fiber,而是 ReactElement。
也就是说,此时 React 只拿到了函数组件的返回值:
{
type: 'div',
key: null,
props: {
children: {
type: 'span',
key: null,
props: {
children: 'Hello React'
}
}
}
}
接下来 React 要做的事情是:
把函数组件返回的 ReactElement 转成子 Fiber
这一步就是:
reconcileChildren(
current,
workInProgress,
nextChildren,
renderLanes
)
这一篇就专门讲 reconcileChildren 和 ChildReconciler。
一、reconcileChildren 在 beginWork 中的位置
先回到 updateFunctionComponent 的主线。
简化后大概是:
function updateFunctionComponent(
current,
workInProgress,
Component,
nextProps,
renderLanes
) {
prepareToReadContext(workInProgress, renderLanes)
const nextChildren = renderWithHooks(
current,
workInProgress,
Component,
nextProps,
context,
renderLanes
)
reconcileChildren(
current,
workInProgress,
nextChildren,
renderLanes
)
return workInProgress.child
}
这里的 nextChildren 就是函数组件返回的 ReactElement。
所以 updateFunctionComponent 可以拆成两个阶段:
renderWithHooks:执行函数组件,得到 ReactElement
reconcileChildren:根据 ReactElement 创建或复用子 Fiber
这和 HostRoot 的逻辑是统一的。
HostRoot 的 children 来自:
nextState.element
FunctionComponent 的 children 来自:
Component(props)
HostComponent 的 children 来自:
props.children
无论来源是什么,只要进入 beginWork,最终都会交给:
reconcileChildren
React 19 当前源码中,reconcileChildren 位于 ReactFiberBeginWork.js,它会根据 current === null 选择 mountChildFibers 或 reconcileChildFibers。
二、reconcileChildren 的核心结构
reconcileChildren 的源码结构很短,但非常关键。
可以简化成:
export function reconcileChildren(
current,
workInProgress,
nextChildren,
renderLanes
) {
if (current === null) {
workInProgress.child = mountChildFibers(
workInProgress,
null,
nextChildren,
renderLanes
)
} else {
workInProgress.child = reconcileChildFibers(
workInProgress,
current.child,
nextChildren,
renderLanes
)
}
}
它只做一个判断:
当前 Fiber 是否有 current
如果没有 current,说明这是一个全新的 Fiber,没有对应的旧 Fiber。
此时走:
mountChildFibers
如果有 current,说明这是一次更新,或者至少存在旧 Fiber 结构。
此时走:
reconcileChildFibers
注意,这里的判断是:
current === null
不是:
current.child === null
这点很重要。
比如初次渲染 HostRootFiber 时,current 并不是 null,因为 createRoot 阶段已经创建了 current HostRootFiber。
但它的 current.child 是 null,因为 App Fiber 还没有创建。
所以 HostRoot 初次渲染时,可能会走 reconcileChildFibers,只是传进去的旧 child 是 null。
而普通函数组件第一次挂载时,如果这个 FunctionComponent Fiber 是新创建的,current 通常是 null,所以它会走 mountChildFibers。
三、mountChildFibers 和 reconcileChildFibers 的区别
这两个函数都来自 ReactChildFiber.js。
React 19 当前源码里,它们都是通过 createChildReconciler 创建出来的:
export const reconcileChildFibers = createChildReconciler(true)
export const mountChildFibers = createChildReconciler(false)
区别只在一个参数:
shouldTrackSideEffects
React 19 当前源码中,reconcileChildFibers 和 mountChildFibers 都由 createChildReconciler 生成,前者传入 true,后者传入 false。
这个参数决定:
是否追踪副作用
更具体一点:
mountChildFibers:不追踪删除、移动等副作用
reconcileChildFibers:追踪删除、移动、插入等副作用
为什么初次挂载不需要追踪副作用?
因为初次挂载时,整棵子树都是新的。
React 不需要比较哪些旧节点要删除。
也不需要判断哪些旧节点要移动。
只要把新 Fiber tree 构建出来,后续 commit 阶段整体插入即可。
所以 mount 阶段可以少做一些副作用标记。
更新阶段不同。
更新时已经存在旧 Fiber tree。
React 必须知道:
哪些 Fiber 可以复用
哪些 Fiber 要删除
哪些 Fiber 要插入
哪些 Fiber 要移动
这些信息会以 flags 的形式记录在 Fiber 上,后续 commit 阶段根据 flags 真正操作 DOM。
四、ChildReconciler 是真正的子节点调和器
reconcileChildren 本身很薄。
真正复杂的是:
createChildReconciler
它内部定义了一系列函数:
deleteChild
deleteRemainingChildren
mapRemainingChildren
useFiber
placeChild
placeSingleChild
updateTextNode
updateElement
updatePortal
updateFragment
createChild
updateSlot
updateFromMap
reconcileSingleElement
reconcileSingleTextNode
reconcileChildrenArray
reconcileChildrenIteratable
reconcileChildFibersImpl
这些函数共同完成一件事:
根据新的 children 和旧的 child Fiber,生成新的 child Fiber 链表
Fiber 的子节点不是数组结构。
它是通过:
child
sibling
return
连接起来的链表结构。
例如:
<div>
<span />
<p />
<button />
</div>
对应的 Fiber 关系大概是:
divFiber.child = spanFiber
spanFiber.sibling = pFiber
pFiber.sibling = buttonFiber
spanFiber.return = divFiber
pFiber.return = divFiber
buttonFiber.return = divFiber
所以 reconcileChildrenArray 最终返回的不是一个数组,而是第一个 child Fiber。
后续兄弟节点通过 sibling 串起来。
五、reconcileChildFibersImpl 如何分流 children 类型
React 的 children 类型很多,不只是 ReactElement。
常见的有:
单个 ReactElement
数组 children
文本节点
Fragment
Portal
Lazy
Context
Thenable
Iterable
空值
reconcileChildFibersImpl 会根据 newChild 的类型分发。
简化后可以理解成:
function reconcileChildFibersImpl(
returnFiber,
currentFirstChild,
newChild,
lanes
) {
if (newChild 是未加 key 的顶层 Fragment) {
newChild = newChild.props.children
}
if (newChild 是 ReactElement) {
return placeSingleChild(
reconcileSingleElement(
returnFiber,
currentFirstChild,
newChild,
lanes
)
)
}
if (newChild 是 Portal) {
return placeSingleChild(
reconcileSinglePortal(...)
)
}
if (newChild 是 Lazy) {
const resolved = resolveLazy(newChild)
return reconcileChildFibersImpl(
returnFiber,
currentFirstChild,
resolved,
lanes
)
}
if (newChild 是数组) {
return reconcileChildrenArray(
returnFiber,
currentFirstChild,
newChild,
lanes
)
}
if (newChild 是可迭代对象) {
return reconcileChildrenIteratable(...)
}
if (newChild 是 thenable) {
return reconcileChildFibersImpl(
returnFiber,
currentFirstChild,
unwrapThenable(newChild),
lanes
)
}
if (newChild 是字符串、数字、bigint) {
return placeSingleChild(
reconcileSingleTextNode(...)
)
}
return deleteRemainingChildren(
returnFiber,
currentFirstChild
)
}
React 19 当前源码中,reconcileChildFibersImpl 会处理 ReactElement、Portal、Lazy、数组、Iterable、AsyncIterable、Thenable、Context、文本节点等分支;剩余情况会按空 children 处理。
这说明一个问题:
reconcileChildren 不是只处理 JSX 标签
它处理的是 ReactNode
JSX 最终只是 ReactElement 的主要来源之一。
六、单个 ReactElement 的调和过程
先看最简单的情况:
function App() {
return <div className="app" />
}
renderWithHooks 执行后返回:
{
type: 'div',
key: null,
props: {
className: 'app'
}
}
这个 newChild 是单个 ReactElement。
所以会走:
reconcileSingleElement(
returnFiber,
currentFirstChild,
element,
lanes
)
它的核心逻辑是:
先拿 element.key
遍历旧 child 链表
如果 key 相同,再比较 type
如果 key 和 type 都能匹配,就复用旧 Fiber
如果 key 相同但 type 不同,删除旧 Fiber,创建新 Fiber
如果 key 不同,删除不匹配的旧 Fiber,继续找
如果没有找到可复用旧 Fiber,就根据 element 创建新 Fiber
简化伪代码如下:
function reconcileSingleElement(
returnFiber,
currentFirstChild,
element,
lanes
) {
const key = element.key
let child = currentFirstChild
while (child !== null) {
if (child.key === key) {
if (child.elementType === element.type) {
const existing = useFiber(
child,
element.props
)
existing.return = returnFiber
return existing
}
deleteRemainingChildren(returnFiber, child)
break
} else {
deleteChild(returnFiber, child)
}
child = child.sibling
}
const created = createFiberFromElement(
element,
returnFiber.mode,
lanes
)
created.return = returnFiber
return created
}
真实源码里还要处理 Fragment、Lazy、Hot Reload、ref、debug info 等分支,但主线就是:
key 决定能不能进入同一组候选
type 决定能不能复用 Fiber
React 19 当前源码中,reconcileSingleElement 会先比较 key,再根据 Fragment、elementType、Lazy resolved type 等条件决定复用旧 Fiber 或创建新 Fiber;复用时会调用 useFiber,创建时会调用 createFiberFromElement。
七、key 和 type 的关系
单个元素复用可以总结成一句话:
key 相同并且 type 相同,Fiber 才能复用
例如:
// 上一次
<div />
// 下一次
<div />
type 都是:
'div'
key 都是:
null
可以复用。
再看:
// 上一次
<div />
// 下一次
<span />
key 都是 null,但 type 不同。
不能复用。
React 会删除旧的 div Fiber,创建新的 span Fiber。
再看组件:
// 上一次
<UserCard />
// 下一次
<UserCard />
type 都是同一个函数对象 UserCard,可以复用。
如果变成:
// 上一次
<UserCard />
// 下一次
<ProductCard />
type 变了,不能复用。
所以不要把 key 理解成"唯一复用条件"。
key 只是先帮 React 找到可能对应的旧 Fiber。
最终能不能复用,还要看 type。
八、useFiber:复用旧 Fiber 不是直接修改旧 Fiber
当 React 判断可以复用旧 Fiber 时,会调用:
useFiber(child, element.props)
它内部会调用:
createWorkInProgress(fiber, pendingProps)
简化后是:
function useFiber(fiber, pendingProps) {
const clone = createWorkInProgress(
fiber,
pendingProps
)
clone.index = 0
clone.sibling = null
return clone
}
这点非常重要。
复用旧 Fiber,不是直接拿旧 Fiber 改。
而是基于 current Fiber 创建或复用它的 alternate,得到 workInProgress Fiber。
也就是说:
复用的是 Fiber 对应关系
不是直接修改 current Fiber
原因还是双缓存模型。
current tree 是当前页面已经生效的树,不能在 render 阶段被直接破坏。
React 只能在 workInProgress tree 上计算新结果。
如果本轮 render 最终成功 commit,workInProgress tree 才会变成新的 current tree。
九、创建新 Fiber:createFiberFromElement
如果没有找到可复用旧 Fiber,React 就会创建新 Fiber。
核心函数是:
createFiberFromElement(
element,
returnFiber.mode,
lanes
)
它会继续根据 element 的信息创建 Fiber:
element.type
element.key
element.props
element.ref
例如:
<div className="app" />
会创建 HostComponent Fiber。
<App />
会创建 FunctionComponent 或其他组件类型 Fiber。
<React.Fragment />
会创建 Fragment Fiber。
Fiber 的 tag,不是 JSX 里直接写出来的,而是 React 根据 element.type 推导出来的。
这就是 ReactElement 到 Fiber 的关键转换点。
十、placeSingleChild:单节点的插入标记
单个 ReactElement 调和之后,还会包一层:
placeSingleChild(newFiber)
它的作用是给新插入的 Fiber 打上 Placement 相关标记。
简化逻辑:
function placeSingleChild(newFiber) {
if (
shouldTrackSideEffects &&
newFiber.alternate === null
) {
newFiber.flags |= Placement
}
return newFiber
}
这里有两个条件:
shouldTrackSideEffects 为 true
newFiber.alternate 为 null
shouldTrackSideEffects 为 true,说明当前是更新调和路径,需要追踪副作用。
newFiber.alternate === null,说明这个 Fiber 没有对应旧 Fiber,是新插入的。
所以它需要在 commit 阶段插入 DOM。
初次挂载路径下,mountChildFibers 的 shouldTrackSideEffects 是 false。
所以很多新 Fiber 不会在这里打 Placement。
这是因为初次挂载时,整棵子树都会作为整体插入,不需要给每个节点都追踪插入副作用。
十一、数组 children 的调和过程
再看更复杂的情况:
function List({ items }) {
return (
<ul>
{items.map(item => (
<li key={item.id}>{item.name}</li>
))}
</ul>
)
}
这里 ul 的 children 是数组。
React 会进入:
reconcileChildrenArray(
returnFiber,
currentFirstChild,
newChildren,
lanes
)
数组 diff 是 React child reconciler 里最重要、也最容易误解的部分。
React 19 当前源码中,reconcileChildrenArray 注释明确说明当前算法不能从两端同时搜索,因为 Fiber 没有 backpointer;它采用的是前向扫描模型,必要时会把剩余旧 children 放入 Map。
它大概分成几个阶段。
十二、第一阶段:从左到右顺序匹配
React 会先从头开始,尝试按顺序匹配新旧节点。
例如旧 children:
A B C
新 children:
A B D
前两个位置:
A 对 A
B 对 B
都可以直接复用。
到了第三个:
C 对 D
发现不匹配,第一阶段结束。
这个阶段的优势是快。
因为很多列表更新都是尾部新增、局部更新,前面大量节点位置不变。
React 可以用非常低的成本完成复用。
十三、第二阶段:旧节点用完,新节点还有剩余
如果旧 child 已经遍历完,但新 children 还有剩余,说明后面的都是新增节点。
例如:
旧:A B
新:A B C D
A、B 复用后,旧节点没了。
C、D 都是新插入。
React 会为它们创建新 Fiber,并通过 placeChild 标记插入位置。
十四、第三阶段:新节点用完,旧节点还有剩余
如果新 children 已经遍历完,但旧 child 还有剩余,说明后面的旧节点都需要删除。
例如:
旧:A B C D
新:A B
A、B 复用后,新节点没了。
C、D 都应该删除。
React 会调用:
deleteRemainingChildren(
returnFiber,
oldFiber
)
把剩余旧 Fiber 加入父 Fiber 的 deletions 列表,并给父 Fiber 打上 ChildDeletion 标记。
deleteChild 在需要追踪副作用时,会把要删除的 child 放进 returnFiber.deletions,并给父 Fiber 添加 ChildDeletion flag;如果不追踪副作用,则直接返回。
十五、第四阶段:中间发生错位,用 Map 匹配剩余节点
最复杂的是这种情况:
旧:A B C D
新:A C B D
A 可以顺序复用。
到第二个位置:
旧 B
新 C
顺序匹配失败。
这时 React 会把剩余旧节点放进 Map:
B -> old B Fiber
C -> old C Fiber
D -> old D Fiber
如果有 key,用 key 作为 Map key。
如果没有 key,用 index 作为 Map key。
然后继续遍历新 children,从 Map 里查找可复用旧 Fiber。
React 19 当前源码中,mapRemainingChildren 会把剩余旧 children 放入 Map;有 key 的用 key,没有 key 的用 index,React 19 还包含 optimistic key 相关分支。
这就是为什么 key 对列表更新非常重要。
有稳定 key 时,React 可以在错位后仍然准确找到对应的旧 Fiber。
没有 key 时,React 只能退化用 index 匹配。
而 index 在插入、删除、重排时并不稳定。
十六、placeChild 如何判断移动
数组 diff 里还有一个关键函数:
placeChild(newFiber, lastPlacedIndex, newIndex)
它会设置:
newFiber.index = newIndex
然后根据旧 Fiber 的 index 判断是否需要移动。
核心思想是:
如果旧 index 小于 lastPlacedIndex,说明这个节点需要移动
否则说明它可以留在原地,并更新 lastPlacedIndex
举个例子。
旧列表:
A B C D
旧 index:
A: 0
B: 1
C: 2
D: 3
新列表:
A C B D
遍历新列表:
A 的旧 index 是 0。
lastPlacedIndex = 0
C 的旧 index 是 2。
2 >= 0
C 不需要移动,更新:
lastPlacedIndex = 2
B 的旧 index 是 1。
1 < 2
说明 B 在新列表里跑到了一个已经处理过的节点后面。
它需要移动。
所以 B 会被标记 Placement。
注意,这里的 Placement 不只表示新增,也可以表示移动。
commit 阶段会根据 Placement 把对应 DOM 插入到正确位置。
十七、为什么 key 不能用 index
看一个例子。
旧数据:
[
{ id: 'a', name: 'A' },
{ id: 'b', name: 'B' },
{ id: 'c', name: 'C' }
]
新数据在头部插入一项:
[
{ id: 'x', name: 'X' },
{ id: 'a', name: 'A' },
{ id: 'b', name: 'B' },
{ id: 'c', name: 'C' }
]
如果 key 用 id:
<li key={item.id}>{item.name}</li>
React 可以知道:
a 还是 a
b 还是 b
c 还是 c
x 是新增
如果 key 用 index:
<li key={index}>{item.name}</li>
旧 key:
0 -> A
1 -> B
2 -> C
新 key:
0 -> X
1 -> A
2 -> B
3 -> C
React 会认为:
key 0 的旧 A 可以复用给新 X
key 1 的旧 B 可以复用给新 A
key 2 的旧 C 可以复用给新 B
这会导致状态错位。
比如每个 li 子组件内部有 input 状态,那么插入头部元素后,状态可能跟着位置走,而不是跟着数据走。
所以 key 的本质不是为了消除 warning。
key 的本质是:
告诉 React,哪个新 child 对应哪个旧 child
十八、文本节点的调和
文本节点也会被调和。
例如:
function App() {
return <div>Hello</div>
}
div 的 children 是字符串:
Hello
React 会进入:
reconcileSingleTextNode
如果旧 child 也是 HostText,就复用旧文本 Fiber。
如果旧 child 不是文本 Fiber,就删除旧 children,创建新的文本 Fiber。
简化逻辑:
function reconcileSingleTextNode(
returnFiber,
currentFirstChild,
textContent,
lanes
) {
if (
currentFirstChild !== null &&
currentFirstChild.tag === HostText
) {
deleteRemainingChildren(
returnFiber,
currentFirstChild.sibling
)
const existing = useFiber(
currentFirstChild,
textContent
)
existing.return = returnFiber
return existing
}
deleteRemainingChildren(
returnFiber,
currentFirstChild
)
const created = createFiberFromText(
textContent,
returnFiber.mode,
lanes
)
created.return = returnFiber
return created
}
React 19 当前源码中,字符串、数字、bigint 类型的 children 会进入文本节点调和,最终通过 reconcileSingleTextNode 创建或复用 HostText Fiber。
十九、Fragment 的特殊处理
Fragment 有两类情况。
第一类是显式 Fragment:
<React.Fragment>
<A />
<B />
</React.Fragment>
第二类是未加 key 的顶层 Fragment:
<>
<A />
<B />
</>
React 在 reconcileChildFibersImpl 一开始会对未加 key 的顶层 Fragment 做特殊处理:
if (isUnkeyedUnrefedTopLevelFragment) {
newChild = newChild.props.children
}
这意味着:
未加 key 的顶层 Fragment 会被当成它的 children 来处理
也就是说:
return (
<>
<A />
<B />
</>
)
在这个位置上,会被展开成:
return [
<A />,
<B />
]
当然,这只是调和层面的理解,不是说源码真的把 JSX 改写成数组。
显式 Fragment 或带 key 的 Fragment,会创建 Fragment Fiber。
二十、删除是挂在父 Fiber 上的
当一个旧 Fiber 需要删除时,React 不会直接在旧 Fiber 上执行删除 DOM。
render 阶段不能改 DOM。
它只会记录删除信息。
删除信息挂在父 Fiber 上:
returnFiber.deletions = [childToDelete]
returnFiber.flags |= ChildDeletion
为什么挂在父 Fiber 上?
因为 commit 阶段删除子节点时,需要从父级上下文出发,找到宿主父节点,然后递归删除对应 DOM。
同时,删除还涉及:
卸载 ref
执行 effect cleanup
调用 class component 的 componentWillUnmount
递归处理子树
这些都不是 render 阶段该做的。
render 阶段只负责标记:
这个 child 后面需要被删除
真正删除发生在 commit 阶段。
二十一、reconcileChildren 的产物是什么
reconcileChildren 的产物不是 DOM。
它的产物是:
新的 child Fiber 链表
具体会挂到:
workInProgress.child
如果有多个子节点,它们通过 sibling 连接:
workInProgress.child
-> firstChild
sibling -> secondChild
sibling -> thirdChild
每个子 Fiber 的 return 都指回父 Fiber:
firstChild.return = workInProgress
secondChild.return = workInProgress
thirdChild.return = workInProgress
与此同时,Fiber 上可能会被打上 flags:
Placement
ChildDeletion
Update
Ref
Passive
在 reconcileChildren 这一层,最核心的是:
Placement
ChildDeletion
Update 更多是在后续 completeWork 或特定 update 逻辑里处理。
所以可以这样总结:
reconcileChildren 负责构建新的 Fiber 子链表,并标记插入、移动、删除等结构变化
二十二、为什么 reconcileChildren 不创建 DOM
这一点要特别强调。
reconcileChildren 只创建 Fiber。
不会创建真实 DOM。
例如:
<div>
<span>Hello</span>
</div>
当 App Fiber 的 reconcileChildren 执行时,它只会创建:
div Fiber
当后续 work loop 进入 div Fiber 的 beginWork 时,才会继续处理 div.props.children,创建:
span Fiber
当进入 span Fiber 的 beginWork 时,才会处理文本 children,创建:
HostText Fiber
真实 DOM 节点创建发生在后面的:
completeWork
也就是说:
beginWork 阶段负责向下构建 Fiber
completeWork 阶段负责向上创建 DOM 和收集 flags
如果把 DOM 创建放在 reconcileChildren 中,就会破坏 render 阶段可中断的设计。
因为 render 阶段可能被中断、重试、丢弃。
如果中途已经改了 DOM,就无法安全回滚。
二十三、从 App 返回值到 Fiber 子树的完整过程
用一个完整例子串起来。
代码:
function App() {
return (
<div className="app">
<h1>Hello</h1>
<p>React 19</p>
</div>
)
}
执行过程:
performUnitOfWork(App Fiber)
beginWork(App Fiber)
updateFunctionComponent
renderWithHooks
执行 App()
得到 ReactElement:
<div className="app">
<h1>Hello</h1>
<p>React 19</p>
</div>
reconcileChildren(
current App Fiber,
workInProgress App Fiber,
nextChildren,
renderLanes
)
nextChildren 是单个 div ReactElement
reconcileSingleElement
没有可复用旧 Fiber
createFiberFromElement
创建 div Fiber
divFiber.return = App Fiber
AppFiber.child = divFiber
updateFunctionComponent 返回 divFiber
workLoop 下一轮处理 divFiber
下一轮:
performUnitOfWork(div Fiber)
beginWork(div Fiber)
updateHostComponent
读取 div.props.children
children 是 h1 和 p
reconcileChildren
进入 reconcileChildrenArray
创建 h1 Fiber 和 p Fiber
divFiber.child = h1Fiber
h1Fiber.sibling = pFiber
h1Fiber.return = divFiber
pFiber.return = divFiber
再往下:
处理 h1 Fiber
读取 h1.props.children
children 是 "Hello"
创建 HostText Fiber
最终得到 workInProgress Fiber tree:
App Fiber
child -> div Fiber
child -> h1 Fiber
child -> HostText Fiber
sibling -> p Fiber
child -> HostText Fiber
注意,这仍然只是 Fiber tree。
DOM 还没有创建。
二十四、reconcileChildren 在更新时的完整例子
初次渲染:
function App() {
return (
<ul>
<li key="a">A</li>
<li key="b">B</li>
<li key="c">C</li>
</ul>
)
}
更新后:
function App() {
return (
<ul>
<li key="a">A</li>
<li key="c">C</li>
<li key="b">B</li>
<li key="d">D</li>
</ul>
)
}
React 调和 ul.props.children 时:
旧 children:
a b c
新 children:
a c b d
处理过程:
a 和 a 顺序匹配,复用 a
b 和 c 不匹配,进入 Map 阶段
把剩余旧 Fiber 放入 Map:
b -> old b Fiber
c -> old c Fiber
处理新 c:
从 Map 找到 old c,复用
处理新 b:
从 Map 找到 old b,复用,但 old b.index 小于 lastPlacedIndex,标记移动
处理新 d:
Map 中找不到,创建新 Fiber,标记插入
Map 中剩余未使用旧 Fiber:
没有剩余,不删除
最终结果:
a 复用
c 复用
b 复用但需要移动
d 新增
后续 commit 阶段会根据 Placement 等 flags 把 DOM 调整到正确位置。
二十五、React 19 中需要注意的新分支
如果只看 React 16 或 React 18 的文章,会漏掉 React 19 当前代码里的一些分支。
比如在 React 19 当前 main 分支中,reconcileChildFibersImpl 里可以看到对 Thenable 的处理:
当 child 是 thenable 时,React 会 unwrapThenable,并重新进入 reconcileChildFibersImpl
源码注释里还说明,child 位置遇到 Usable 时,会用类似 use 的算法进行解包;对 promise 来说,会抛出异常来 unwind stack,等 promise resolve 后再 replay。
这说明 React 19 的 child reconciliation 不只是传统意义上的:
ReactElement diff
它已经和 Suspense、use、Thenable、AsyncIterable 等能力连接在一起。
不过主线仍然不变:
输入 newChild
根据类型分流
创建或复用 Fiber
建立 child / sibling / return 关系
标记结构性副作用
二十六、这一篇的完整调用链
把这一篇串成一条完整链路:
FunctionComponent beginWork
updateFunctionComponent
renderWithHooks
执行 Component(props)
得到 nextChildren
reconcileChildren(
current,
workInProgress,
nextChildren,
renderLanes
)
如果 current === null:
mountChildFibers(
workInProgress,
null,
nextChildren,
renderLanes
)
否则:
reconcileChildFibers(
workInProgress,
current.child,
nextChildren,
renderLanes
)
mountChildFibers 和 reconcileChildFibers 都来自:
createChildReconciler
createChildReconciler 根据 newChild 类型分流:
单个 ReactElement:
reconcileSingleElement
key 和 type 匹配则 useFiber 复用
否则 createFiberFromElement 新建
文本节点:
reconcileSingleTextNode
复用或创建 HostText Fiber
数组:
reconcileChildrenArray
先顺序匹配
再处理新增或删除
必要时构建 Map 匹配剩余旧节点
通过 placeChild 判断插入或移动
Fragment:
可能展开 children
也可能创建 Fragment Fiber
Lazy:
resolveLazy 后重新 reconcile
Thenable:
unwrapThenable 后重新 reconcile
空值:
deleteRemainingChildren
最终结果:
workInProgress.child 指向新的 child Fiber 链表
二十七、这一篇最重要的结论
第一,reconcileChildren 是 beginWork 阶段把 ReactElement 转成子 Fiber 的核心入口。
第二,reconcileChildren 本身很薄,真正复杂的逻辑在 ReactChildFiber.js 的 createChildReconciler 中。
第三,mountChildFibers 和 reconcileChildFibers 都来自 createChildReconciler,区别是是否追踪副作用。
第四,初次挂载通常不需要追踪删除、移动等副作用,更新阶段需要追踪。
第五,单个 ReactElement 的复用规则主要看 key 和 type。
第六,key 相同只是进入候选匹配,最终能不能复用还要看 type 是否一致。
第七,复用 Fiber 不是直接修改 current Fiber,而是通过 useFiber 创建或复用 workInProgress Fiber。
第八,数组 children 会先尝试从左到右顺序匹配,遇到错位后再用 Map 匹配剩余旧节点。
第九,placeChild 会根据旧 index 和 lastPlacedIndex 判断节点是否需要移动。
第十,key 的本质是帮助 React 在新旧 children 之间建立稳定身份映射,不只是为了消除 warning。
第十一,删除不会在 render 阶段真正执行,而是记录到父 Fiber 的 deletions 上,并打上 ChildDeletion 标记。
第十二,reconcileChildren 只创建或复用 Fiber,不创建真实 DOM。
第十三,真实 DOM 的创建发生在后续 completeWork 阶段。
第十四,React 19 的 child reconciliation 还包含 Thenable、AsyncIterable、Context 等分支,已经和 Suspense、use 等能力连接起来。