上一篇讲到,reconcileChildren 会在 beginWork 阶段根据 ReactElement 创建或复用子 Fiber。
例如:
function App() {
return (
<div className="app">
<h1>Hello</h1>
<p>React 19</p>
</div>
)
}
在 beginWork 阶段,React 会一层一层创建出 Fiber 结构:
App Fiber
child -> div Fiber
child -> h1 Fiber
child -> HostText Fiber
sibling -> p Fiber
child -> HostText Fiber
但是到这里为止,React 还没有创建真实 DOM。
div Fiber 不是 div DOM。
h1 Fiber 不是 h1 DOM。
HostText Fiber 也不是文本 DOM 节点。
真实 DOM 的创建发生在 render 阶段的另一半:
completeWork
这一篇就围绕 completeWork 展开。
重点讲清楚:
beginWork 和 completeWork 的分工是什么
completeUnitOfWork 如何向上归并
HostComponent 的真实 DOM 是在哪里创建的
HostText 的文本节点是在哪里创建的
appendAllChildren 为什么只追加直接 DOM 后代
bubbleProperties 如何收集 flags
render 阶段为什么只是创建 DOM,还不插入页面
一、render 阶段分为 begin 和 complete
React render 阶段不是只有 beginWork。
它实际有两个方向:
beginWork:向下
completeWork:向上
beginWork 负责从当前 Fiber 计算子 Fiber。
也就是:
根据当前 Fiber 的 props、state、updateQueue、context 等信息,得到下一层 children,并生成 child Fiber
completeWork 负责在子节点都处理完成之后,完成当前 Fiber。
也就是:
创建真实 DOM
收集子树 flags
准备 commit 阶段需要的信息
可以用一个组件结构理解:
function App() {
return (
<div>
<span>Hello</span>
</div>
)
}
render 阶段大概是:
beginWork App
创建 div Fiber
beginWork div
创建 span Fiber
beginWork span
创建 HostText Fiber
beginWork HostText
没有子节点,返回 null
completeWork HostText
创建文本节点
completeWork span
创建 span DOM,并把文本节点 append 到 span DOM
completeWork div
创建 div DOM,并把 span DOM append 到 div DOM
completeWork App
函数组件没有 DOM,只做 flags 冒泡
所以真实 DOM 不是从上往下创建的。
而是从下往上创建的。
原因也很简单:
父 DOM 创建完成后,需要把子 DOM append 进去。
但父 Fiber 在 complete 时,必须确保它的子 Fiber 已经完成,子 DOM 已经准备好。
所以 DOM 创建发生在向上归并阶段更合理。
二、completeUnitOfWork 是 complete 阶段的控制器
completeWork 不是直接被 workLoop 调用的。
workLoop 调的是:
performUnitOfWork
当 beginWork 返回 null 时,说明当前 Fiber 没有 child 需要继续向下处理。
这时会进入:
completeUnitOfWork(unitOfWork)
简化结构如下:
function performUnitOfWork(unitOfWork) {
const current = unitOfWork.alternate
const next = beginWork(
current,
unitOfWork,
renderLanes
)
unitOfWork.memoizedProps = unitOfWork.pendingProps
if (next === null) {
completeUnitOfWork(unitOfWork)
} else {
workInProgress = next
}
}
completeUnitOfWork 的职责是:
完成当前 Fiber
如果有 sibling,进入 sibling
如果没有 sibling,继续向父 Fiber 归并
直到找到下一个 sibling,或者一路归并到 Root
简化伪代码:
function completeUnitOfWork(unitOfWork) {
let completedWork = unitOfWork
do {
const current = completedWork.alternate
const returnFiber = completedWork.return
completeWork(
current,
completedWork,
renderLanes
)
const siblingFiber = completedWork.sibling
if (siblingFiber !== null) {
workInProgress = siblingFiber
return
}
completedWork = returnFiber
workInProgress = completedWork
} while (completedWork !== null)
}
React 19 当前源码中,completeUnitOfWork 位于 ReactFiberWorkLoop.js,它会调用 completeWork(current, completedWork, entangledRenderLanes) 完成当前 Fiber;如果存在 sibling,就把 workInProgress 切到 sibling,否则继续向 return Fiber 归并。
三、completeWork 是真正完成当前 Fiber 的地方
completeWork 位于:
packages/react-reconciler/src/ReactFiberCompleteWork.js
它会根据 Fiber tag 分发不同逻辑。
简化结构:
function completeWork(
current,
workInProgress,
renderLanes
) {
const newProps = workInProgress.pendingProps
switch (workInProgress.tag) {
case HostComponent:
return completeHostComponent(
current,
workInProgress,
newProps
)
case HostText:
return completeHostText(
current,
workInProgress
)
case HostRoot:
bubbleProperties(workInProgress)
return null
case FunctionComponent:
bubbleProperties(workInProgress)
return null
case Fragment:
bubbleProperties(workInProgress)
return null
}
}
不是所有 Fiber 都会创建 DOM。
只有宿主相关 Fiber 才和真实宿主节点有关。
对于 ReactDOM 来说,主要是:
HostComponent:div、span、button 等 DOM 元素
HostText:文本节点
HostRoot:根容器,不直接创建 DOM
函数组件、Fragment、Memo、Context 等 Fiber 本身没有对应真实 DOM。
它们在 complete 阶段主要做:
向上冒泡 flags
维护子树信息
把 commit 阶段需要知道的副作用聚合到父级
React Reconciler README 对 Host Config 的 createInstance、appendInitialChild、finalizeInitialChildren 有明确说明:createInstance 创建宿主实例,appendInitialChild 在初始挂载时把子节点加入父实例,finalizeInitialChildren 调用时子节点已经被加入实例,但实例还没有连接到屏幕上的树。
四、HostText:文本 DOM 节点在哪里创建
先看最简单的文本节点:
<span>Hello</span>
Hello 会在 Fiber 树里对应一个:
HostText Fiber
当 work loop 处理到 HostText Fiber 时:
beginWork HostText
会直接返回 null。
因为文本节点没有 children。
于是立刻进入:
completeWork HostText
在 mount 阶段,completeWork 会创建文本实例:
const textInstance = createTextInstance(
newText,
rootContainerInstance,
currentHostContext,
workInProgress
)
workInProgress.stateNode = textInstance
对于 ReactDOM 来说,createTextInstance 最终会创建真实文本节点。
可以理解成:
document.createTextNode('Hello')
创建完之后,文本节点会保存在:
HostTextFiber.stateNode
所以:
Fiber.stateNode 对 HostText 来说,指向真实文本 DOM 节点
但注意:
此时文本节点还没有插入页面
它只是被创建出来,并挂在 Fiber 上。
真正插入页面要等 commit 阶段。
五、HostComponent:元素 DOM 节点在哪里创建
再看:
<div className="app">
<span>Hello</span>
</div>
div 对应:
HostComponent Fiber
当 div Fiber 进入 completeWork 时,它的子节点已经完成。
也就是说:
span Fiber 已经 complete
span.stateNode 已经有 span DOM
span 下面的 HostText 也已经有文本 DOM
这时 div Fiber 的 completeWork 会创建 div DOM:
const instance = createInstance(
type,
newProps,
rootContainerInstance,
currentHostContext,
workInProgress
)
appendAllChildren(
instance,
workInProgress,
false,
false
)
finalizeInitialChildren(
instance,
type,
newProps,
currentHostContext
)
workInProgress.stateNode = instance
对于 ReactDOM 来说:
createInstance('div', ...)
可以理解成创建:
document.createElement('div')
然后:
workInProgress.stateNode = instance
所以:
Fiber.stateNode 对 HostComponent 来说,指向真实 DOM Element
例如:
div Fiber.stateNode -> HTMLDivElement
span Fiber.stateNode -> HTMLSpanElement
HostText Fiber.stateNode -> Text
六、为什么 DOM 是从子到父创建的
以这个结构为例:
<div>
<span>Hello</span>
</div>
执行顺序是:
completeWork HostText
创建 Text 节点
completeWork span
创建 span DOM
append Text 到 span DOM
completeWork div
创建 div DOM
append span DOM 到 div DOM
也就是说,子节点先完成。
父节点后完成。
这样父节点在 complete 时,就可以直接通过 appendAllChildren 把已经完成的子 DOM 收集起来。
如果父 DOM 先创建,也不是不行。
但 React 的 Fiber 遍历模型是深度优先。
当它回到父节点 complete 时,天然就拥有完整的子树结果。
这和 React 的 render 阶段设计完全一致:
beginWork 向下展开 Fiber
completeWork 向上归并结果
七、appendAllChildren 为什么只追加直接 DOM 后代
appendAllChildren 是 completeWork 里非常关键的函数。
它的作用是:
从当前 Fiber 的子树中,找到所有直接宿主子节点,并 append 到当前 DOM 实例里
这里的"直接宿主子节点"很重要。
看一个例子:
function App() {
return (
<div>
<Header />
</div>
)
}
function Header() {
return <span>Hello</span>
}
Fiber 树大概是:
div Fiber
child -> Header Fiber
child -> span Fiber
child -> HostText Fiber
Header Fiber 是函数组件。
它没有 DOM。
当 div Fiber complete 时,appendAllChildren(divDOM, divFiber) 不能只看 divFiber.child。
因为 divFiber.child 是:
Header Fiber
Header 没有 DOM。
React 要继续向下找,直到找到:
span Fiber
也就是最近的宿主节点。
最终应该执行的是:
divDOM.appendChild(spanDOM)
而不是:
divDOM.appendChild(Header)
因为 Header 不是 DOM。
所以 appendAllChildren 会遍历当前 Fiber 的子树:
遇到 HostComponent 或 HostText,就 append 它的 stateNode
遇到 FunctionComponent、Fragment 等非宿主 Fiber,就继续往下找它们的 child
处理完一个分支,再找 sibling
不要越过当前 Fiber 的边界
这就是为什么 React 可以支持组件嵌套。
函数组件本身不会生成 DOM,但它下面的宿主节点最终会被收集到最近的宿主父节点里。
八、appendAllChildren 不会重复 append 深层 DOM
还是这个结构:
<div>
<span>Hello</span>
</div>
当 span Fiber complete 时:
spanDOM.appendChild(textNode)
当 div Fiber complete 时:
divDOM.appendChild(spanDOM)
注意:
divDOM 不会直接 append textNode
因为 textNode 已经是 spanDOM 的子节点了。
appendAllChildren 在遇到 HostComponent 时,会 append 当前 HostComponent 的 stateNode,然后不会继续深入它的 children。
否则就会把深层 DOM 重复 append 到父节点。
所以它的核心规则是:
遇到宿主节点,append 当前宿主节点,然后停止深入这一支
遇到非宿主节点,继续深入寻找宿主节点
九、finalizeInitialChildren 做什么
DOM 创建完成、子 DOM append 完成后,会调用:
finalizeInitialChildren(
instance,
type,
newProps,
currentHostContext
)
这个函数来自 Host Config。
对于 ReactDOM 来说,它会处理初始属性。
例如:
<button
className="primary"
disabled
onClick={handleClick}
>
Submit
</button>
finalizeInitialChildren 会完成初始属性设置相关工作。
可以粗略理解成:
设置 className
设置 disabled
设置 style
设置事件相关属性
处理 autoFocus 等特殊属性
不同宿主环境实现不同。
ReactDOM 创建 DOM。
React Native 创建 Native View。
自定义 renderer 可以创建自己的宿主实例。
这也是 React Reconciler 和 Renderer 分离的原因:
Reconciler 负责 Fiber、调和、调度、commit 流程
Renderer 通过 Host Config 提供 createInstance、appendInitialChild、commitUpdate 等宿主操作
十、mount 阶段和 update 阶段的 completeWork 不一样
completeWork 会根据:
current !== null && workInProgress.stateNode !== null
区分 mount 和 update。
mount 阶段
mount 阶段没有旧 DOM。
所以需要:
创建 DOM
append 子 DOM
初始化属性
stateNode 指向 DOM
大概是:
const instance = createInstance(...)
appendAllChildren(instance, workInProgress, ...)
finalizeInitialChildren(instance, type, newProps, ...)
workInProgress.stateNode = instance
update 阶段
update 阶段已经有旧 DOM。
所以不需要重新创建 DOM。
而是复用:
const instance = workInProgress.stateNode
然后比较旧 props 和新 props。
如果有变化,就标记更新。
在较新的 React DOM 实现里,更新相关信息会通过 Host Config 的 prepareUpdate 或 commitUpdate 路径处理,最终在 commit 阶段应用到 DOM。
核心思想不变:
render 阶段计算差异
commit 阶段应用差异
也就是说,update 阶段的 completeWork 不直接修改 DOM。
它只是准备 commit 阶段需要的更新信息,并打上对应 flags。
十一、为什么 render 阶段创建 DOM 但不插入页面
这里很容易混乱。
render 阶段确实可能创建 DOM 节点。
比如 mount 阶段的:
document.createElement('div')
document.createTextNode('Hello')
但是它不会把这些节点插入真实页面。
也就是不会执行最终的:
container.appendChild(...)
或者:
parent.insertBefore(...)
原因是 render 阶段可以被中断、重试、丢弃。
如果 render 阶段已经把 DOM 插入页面,那并发渲染就不安全了。
例如低优先级更新 render 到一半,React 决定让出主线程。
如果这时 DOM 已经部分插入页面,用户会看到半成品 UI。
所以 React 必须保证:
render 阶段可以计算,可以创建离屏 DOM,但不能把结果提交到屏幕
commit 阶段才可以真正修改页面
这就是 render 和 commit 的根本边界。
十二、completeWork 如何处理 FunctionComponent
函数组件没有真实 DOM。
例如:
function Header() {
return <h1>Hello</h1>
}
Header Fiber 的 completeWork 不会创建 DOM。
它主要做:
bubbleProperties(workInProgress)
return null
也就是把子树的 flags、lanes 等信息向上冒泡。
因为 Header 本身没有 DOM,但它的子树可能有 DOM 插入、更新、删除。
比如:
Header Fiber
child -> h1 Fiber
child -> HostText Fiber
h1 Fiber 可能有 Placement。
HostText Fiber 可能有文本节点。
这些副作用需要让父级知道。
所以函数组件 complete 时不能什么都不做。
它虽然不创建 DOM,但要完成子树副作用收集。
十三、bubbleProperties:flags 如何向上冒泡
bubbleProperties(workInProgress) 是 complete 阶段非常关键的一步。
它的作用是:
把子 Fiber 的 flags 和 subtreeFlags 汇总到当前 Fiber 的 subtreeFlags 上
简化逻辑:
function bubbleProperties(completedWork) {
let subtreeFlags = NoFlags
let child = completedWork.child
while (child !== null) {
subtreeFlags |= child.subtreeFlags
subtreeFlags |= child.flags
child.return = completedWork
child = child.sibling
}
completedWork.subtreeFlags |= subtreeFlags
}
这一步很重要。
commit 阶段不可能盲目遍历整棵 Fiber 树。
它需要快速知道:
某个子树里有没有需要提交的副作用
如果一个 Fiber 的:
subtreeFlags === NoFlags
说明这棵子树没有需要处理的 commit 副作用。
commit 阶段就可以跳过很多遍历。
所以 bubbleProperties 是 render 阶段为 commit 阶段做索引。
它把分散在子 Fiber 上的 flags 向父级聚合。
十四、flags 和 subtreeFlags 的区别
Fiber 上有两个容易混淆的字段:
flags
subtreeFlags
flags 表示当前 Fiber 自己的副作用。
例如:
当前 Fiber 需要插入
当前 Fiber 需要更新
当前 Fiber 有 ref
当前 Fiber 有 passive effect
subtreeFlags 表示子树中的副作用。
例如:
当前 Fiber 的某个后代节点需要插入
当前 Fiber 的某个后代节点需要更新
当前 Fiber 的某个后代节点有 effect
举个例子:
function App() {
return <div><span>Hello</span></div>
}
如果 span Fiber 有 Placement:
span.flags 包含 Placement
那么 complete div Fiber 时,会把 span 的 flags 冒泡到:
div.subtreeFlags
complete App Fiber 时,又会继续冒泡到:
App.subtreeFlags
最后 Root 就能知道:
这棵树下面有需要 commit 的副作用
十五、HostRoot 的 completeWork 做什么
HostRootFiber 不创建 DOM。
它对应的是 React 根 Fiber,不是实际 DOM 节点。
真实根容器在:
FiberRootNode.containerInfo
例如:
<div id="root"></div>
HostRoot complete 时,主要做:
popHostContainer
处理 pending context
bubbleProperties
标记 render 阶段完成相关状态
真正把整棵新 DOM 子树插入 root container,是 commit 阶段的事情。
也就是说:
HostRoot complete 完成后,render 阶段基本就接近结束
但页面仍然没有被修改
十六、completeWork 和 commit 的关系
completeWork 会创建 DOM,但不插入页面。
commit 会把 DOM 插入页面。
它们之间的关系是:
completeWork:
创建 DOM 实例
设置初始属性
把子 DOM append 到父 DOM
收集 flags
构建完成的 workInProgress tree
commit:
找到带有 flags 的 Fiber
执行 DOM 插入、更新、删除
切换 root.current
执行 layout effect
调度 passive effect
例如初次渲染:
root.render(<App />)
在 render 阶段,completeWork 可能已经构建出一棵离屏 DOM 树:
div
h1
"Hello"
p
"React 19"
但这棵 DOM 树还没有挂到:
<div id="root"></div>
等到 commit mutation 阶段,React 才会把它插入到 root container。
十七、为什么 completeWork 不是 commit
很多人会把这两个阶段混在一起。
因为 completeWork 里确实创建了 DOM。
但创建 DOM 不等于提交 DOM。
这里要区分两个动作:
创建节点
插入节点
render 阶段可以创建节点,因为这些节点还没有影响页面。
commit 阶段才能插入节点,因为插入节点会改变页面。
所以:
completeWork 属于 render 阶段
commitMutationEffects 才是真正修改页面的阶段
这个边界是 React 并发渲染安全性的基础。
十八、完整例子:从 beginWork 到 completeWork
代码:
function App() {
return (
<div className="app">
<h1>Hello</h1>
<p>React 19</p>
</div>
)
}
begin 阶段:
beginWork App
执行 App()
创建 div Fiber
beginWork div
读取 div.props.children
创建 h1 Fiber 和 p Fiber
beginWork h1
读取 h1.props.children
创建 HostText Fiber "Hello"
beginWork HostText
返回 null
complete 阶段开始:
completeWork HostText "Hello"
createTextInstance("Hello")
HostText.stateNode = textNode
然后回到 h1:
completeWork h1
createInstance("h1")
appendAllChildren(h1DOM, h1Fiber)
把 textNode append 到 h1DOM
finalizeInitialChildren(h1DOM, "h1", props)
h1Fiber.stateNode = h1DOM
bubbleProperties(h1Fiber)
然后处理 p:
beginWork p
创建 HostText Fiber "React 19"
completeWork HostText "React 19"
createTextInstance("React 19")
completeWork p
createInstance("p")
append textNode 到 pDOM
pFiber.stateNode = pDOM
然后回到 div:
completeWork div
createInstance("div")
appendAllChildren(divDOM, divFiber)
append h1DOM 到 divDOM
append pDOM 到 divDOM
finalizeInitialChildren(divDOM, "div", props)
divFiber.stateNode = divDOM
最后回到 App:
completeWork App
不创建 DOM
bubbleProperties(AppFiber)
最终 render 阶段得到:
App Fiber
child -> div Fiber stateNode=divDOM
child -> h1 Fiber stateNode=h1DOM
child -> HostText Fiber stateNode=textNode
sibling -> p Fiber stateNode=pDOM
child -> HostText Fiber stateNode=textNode
十九、completeWork 结束后得到什么
当整棵树 complete 完成后,React 得到的是:
一棵完成计算的 workInProgress Fiber tree
每个 HostComponent / HostText Fiber 上挂着 stateNode
Fiber 上带着 flags 和 subtreeFlags
Root 上可以拿到 finishedWork
但页面仍然还没变。
此时 React 只是准备好了:
即将提交到页面的结果
后续会进入:
commitRoot
然后再进入 commit 的几个阶段:
beforeMutation
mutation
layout
passive
其中真正插入、更新、删除 DOM 的是:
mutation 阶段
二十、这一篇的完整调用链
把这一篇串起来:
workLoopSync 或 workLoopConcurrent
performUnitOfWork(fiber)
beginWork(current, fiber, renderLanes)
如果 beginWork 返回 child:
workInProgress = child
继续向下
如果 beginWork 返回 null:
completeUnitOfWork(fiber)
completeUnitOfWork
completeWork(current, completedWork, renderLanes)
根据 completedWork.tag 分支:
HostText:
createTextInstance
workInProgress.stateNode = textInstance
bubbleProperties
HostComponent:
createInstance
appendAllChildren
finalizeInitialChildren
workInProgress.stateNode = instance
bubbleProperties
FunctionComponent:
不创建 DOM
bubbleProperties
Fragment:
不创建 DOM
bubbleProperties
HostRoot:
不创建 DOM
pop host context
bubbleProperties
complete 当前 Fiber 后:
如果有 sibling:
workInProgress = sibling
继续 performUnitOfWork
如果没有 sibling:
completedWork = returnFiber
继续向上 complete
直到 HostRoot complete 完成
render 阶段得到 finished workInProgress tree
二十一、这一篇最重要的结论
第一,render 阶段分为 beginWork 和 completeWork 两个方向。
第二,beginWork 负责向下创建或复用子 Fiber。
第三,completeWork 负责向上完成当前 Fiber。
第四,真实 DOM 的创建主要发生在 completeWork 阶段。
第五,HostText 会在 complete 阶段通过 createTextInstance 创建文本节点。
第六,HostComponent 会在 complete 阶段通过 createInstance 创建元素节点。
第七,appendAllChildren 会把当前 Fiber 子树中最近的宿主子节点 append 到当前 DOM 实例上。
第八,函数组件、Fragment 等 Fiber 本身没有 DOM,它们在 complete 阶段主要做 flags 冒泡。
第九,bubbleProperties 会把子 Fiber 的 flags 和 subtreeFlags 汇总到父 Fiber 的 subtreeFlags 上。
第十,flags 描述当前 Fiber 自己的副作用,subtreeFlags 描述子树里的副作用。
第十一,completeWork 可以创建 DOM,但不会把 DOM 插入页面。
第十二,真正插入、更新、删除 DOM,要等 commit 阶段。
第十三,render 阶段可以被中断、重试、丢弃,所以不能在 render 阶段修改页面。
第十四,commit 阶段不可中断,因为它会真正修改宿主环境。