reconcileChildren 深入:React 如何根据 ReactElement 构建子 Fiber

上一篇讲到,函数组件在 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
)

这一篇就专门讲 reconcileChildrenChildReconciler

一、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 选择 mountChildFibersreconcileChildFibers

二、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 当前源码中,reconcileChildFibersmountChildFibers 都由 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。

初次挂载路径下,mountChildFibersshouldTrackSideEffects 是 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 FiberreconcileChildren 执行时,它只会创建:

复制代码
div Fiber

当后续 work loop 进入 div FiberbeginWork 时,才会继续处理 div.props.children,创建:

复制代码
span Fiber

当进入 span FiberbeginWork 时,才会处理文本 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 链表

二十七、这一篇最重要的结论

第一,reconcileChildrenbeginWork 阶段把 ReactElement 转成子 Fiber 的核心入口。

第二,reconcileChildren 本身很薄,真正复杂的逻辑在 ReactChildFiber.jscreateChildReconciler 中。

第三,mountChildFibersreconcileChildFibers 都来自 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 等能力连接起来。

相关推荐
三翼鸟数字化技术团队2 小时前
事件循环原来这么简单!
前端
gf13211112 小时前
python_【更新已发送的消息卡片】
java·前端·python
zithern_juejin2 小时前
typeof、instanceof与Object.prototype.toString()
javascript
一点一木2 小时前
2026 终端 AI 编码 Agent 六大工具深度横评
前端·人工智能·claude
Highcharts.js3 小时前
Highcharts React v5升级三问|最大的升级方向是什么?需要注意什么?有什么优化?
前端·javascript·react.js·前端框架·highcharts·大数据渲染·前端性能
马玉霞3 小时前
vue web端页面组件展示
前端·vue.js
129y3 小时前
JS入门参考:引擎、作用域与let/const,一起慢慢理解~
javascript
代码煮茶3 小时前
Vue3 权限系统实战 | 从 0 搭建完整 RBAC 权限管理
前端·javascript·vue.js
前端小木屋3 小时前
Node基础入门
javascript·node.js