我对React18 Fiber架构的理解
基本数据结构
React中有三种节点类型
ReactElement
:createElement
的返回值
// 主要的,不包含全部
type ReactElement = {
$$typeof: any, // Symbol.for('react.element')
type: any, // dom是标签名、函数组件是函数本身、类组件是类本身
key: any,
ref: any,
props: any,
_owner: any,
}
ReactComponent
:函数组件或类组件FiberNode
:Fiber架构的工作单元,一般情况下与ReactElement对应(有特例下面会提到)
// 主要的,不包含全部
type FiberNode = {
// 作为静态属性
tag: WorkTag, // 标识FunctionComponent/HostComponent/HostText等
key: null | string,
type: any, // 同ReactElement
elementType: any, // 大部分情况同type,某些情况不同,比如FunctionComponent使用React.memo包裹
stateNode: any, // 存dom节点
index: number,
ref: any,
refCleanup: null | (() => void),
// 用于组成Fiber树
return: Fiber | null, // 父
child: Fiber | null, // 子
sibling: Fiber | null, // 右边第一个兄弟
// 作为工作单元,保存本次更新造成的状态改变相关信息
// 要更新的新props
pendingProps: any,
// 计算后的props
memoizedProps: any,
// 更新队列
updateQueue: mixed,
// 计算出的新状态
memoizedState: any,
// 保存context、事件相关内容
dependencies: Dependencies | null,
mode: TypeOfMode,
// 副作用
flags: Flags,
// 子树的所有节点的flags
subtreeFlags: Flags,
// 要删除的子fiberNode
deletions: Array<Fiber> | null,
nextEffect: Fiber | null,
firstEffect: Fiber | null,
lastEffect: Fiber | null,
// 用于调度 优先级
lanes: Lanes,
childLanes: Lanes,
// 指向对应的workInProgress FiberNode或者current FiberNode
alternate: Fiber | null,
他们三者关系如下:
// ReactComponent:函数组件
const App = () => {
// ReactElement:jsx语法会被转化为createElement方法
return <h1>hello! react18</h1>
}
// ReactElement
const element = <App />
// FiberNode:
// <App /> 和 <h1>hello! react18</h1> 都有与之对应的FiberNode
Fiber树
function App() {
return (
<Header>
<img />
<span>hello! react18</span>
</Header>
);
}
function Header({ children }) {
return <header>{children}</header>;
}
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<App />);
这段JSX代码运行后会生成一个如下所示的 Fiber树,其中有两个特殊的节点:
fiberRootNode
:整个 Fiber树 的根结点,此节点的current属性指向的就是current Fiber树
,对应的还有workInprogress Fiber树
,下面会解释。hostRootFiber
:挂载 React 应用的 dom 对应的fiberNode
。

❓:我曾有一个疑问,在上述这段JSX结构中,Header 组件下还有img和span节点,函数内部又有header节点,那么 Header 的fiberNode.child
指向的是img的fiberNode
呢还是内部的header的呢?
✅:指向的是header的,img和span会存到Header组件的props.children
中,如果我渲染了children,那么img和span也会生成对应的fiberNode
,并且在 Fiber树中的节点位置会是渲染的位置,并不是定义时的位置;如果不渲染,那也就不会生成与之对应的fiberNode
。这也说明了FiberNode
是Fiber架构运行时动态的工作单元,并不像ReactElement
一样只是静态结构。
两颗Fiber树(双缓存)
UI = f(state)
当下大多数前端框架的实现原理就是这个公式,状态改变引起UI改变,UI因宿主环境的不同而不同,在浏览器中就是dom。
React是一个应用级框架,意思是当状态改变时,React不知道这个改变会导致哪些fiberNode
改变,所以要从根节点开始找这个改变的状态与UI的对应关系。与之对应的是组件级框架,比如Vue,Vue的实现方式可以在状态改变时就定位到与改变对应的组件,然后从组件中寻找与改变对应的UI。
对于React来说,寻找state和UI对应关系的大致流程是从根节点开始遍历Fiber树,一路上标记发生改变的fiberNode
,最终再将改变映射到真实UI上。
为了不影响到当前显示使用的Fiber树(名为current Fiber树
),在映射到真实UI之前React的活动都是在内存中进行的,内存中也存在一个Fiber树,名为workInProgress Fiber树
,两颗Fiber树之间互相以alternate
属性连接。工作完成后根节点的current
属性就会指向workInProgress Fiber树
,workInProgress Fiber树
和current Fiber树
位置就互换了,这种技术也被称为双缓存,简而言之就是后台工作,完成以后前台后台位置互换。
工作流程
在初始化挂载(mount)时,React会先创建根节点fiberRootNode
,然后从根节点开始创建wip Fiber树
(workInProgress简称);
在更新发生(update)时,React会从发生动作的fiberNode
开始向上找到根节点(fiberRootNode.stateNode
即hostRootFiber
),然后从根节点开始生成新的wip Fiber树
,当然此时的生成不是从0开始创建,会根据一些条件复用或删除已存在的wip Fiber树
中的某些节点。
在wip Fiber树
构建过程中会根据改变打上对应的标记,比如插入或删除标记。构建完成后会遍历wip
,根据不同的标记进行操作,最终将对应的UI渲染到页面上。
这就是大致的工作流程,这个过程中主要有两个大的阶段:render
阶段和commit
阶段
render
开始工作前会先找到div#root对应的fiberNode
,称为hostRootFiber
,然后开始生成wip Fiber树
。
这个过程分为两个部分:beginWork
和completeWork
。
这是一个深度优先遍历的过程:先从hostRootFiber
开始向下以深度优先的方式遍历到每个fiberNode
,执行beginWork
方法,当遍历到叶子节点(fiberNode.child===null
)时,再调用completeWork
方法,然后判断该fiberNode
是否存在兄弟节点(fiberNode.sibling!==null
),如果存在,则从兄弟节点开始继续向下遍历执行beginWork
,如果不存在,就向上遍历一个节点执行completeWork
,以此往复直到整颗wip Fiber树
生成完成。
用一张图展示这个流程,还是上面一样的代码
function App() {
return (
<Header>
<img />
<span>hello! react18</span>
</Header>
);
}
function Header({ children }) {
return <header>{children}</header>;
}
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<App />);

beginWork
该过程作用是为传入的wip fiberNode
生成子fiberNode
,并打上副作用标记,beginWork
会打两种标记:
Placement
插入ChildDeletion
删除
对于 <div> <span/> </div>
的ReactElement
结构,当进入div的beginWork
时,通过对比span的current fiberNode
和span的reactElement
,生成span对应的新的wip fiberNode
。
- 进入
beginWork
后,根据不同的fiberNode.tag
进入不同的逻辑,这一步主要是为了获取fiberNode
对应的子reactElement
结构,不同类型节点的子reactElement
会存到不同的地方,比如对于HostRoot
来说是存到fiberNode.memorizedState
,对于HostComponent
是存到fiberNode.pendingProps.children
,而对于FC
(FunctionComponent简称)来说,来自于函数的返回值,所以说函数组件的函数就是在这个时候调的,通过renderWithHooks
函数来调用FC
,这个函数内除了返回函数组件的返回值,也做了其他和hooks
有关的操作,这里先不谈。这里也就知道了,平时开发打断点调试时想找到函数组件调用位置时,在renderWithHooks
打断点就可以。 - 获取到
子reactElement
后,将它连同wip fiberNode
一起传给reconcileChildren
进行reconcile
的操作。这里会拿到wip fiberNode.alternate
即与之对应的current fiberNode
,根据update或mount进入不同的函数(current===null
就是mount),reconcileChildrenFibers
和mountChildrenFibers
,他们俩内部都调的reconcileChildrenFibers
函数,区别是:是否会为生成的子fiberNode
打副作用标记,根据给reconcileChildrenFibers
传不同的参数(shouldTrackSideEffects
)来区分。mount时不需要给子fiberNode
打标记,这是 React 的一条优化路径,因为mount都是插入操作(Placement
),这个时候只给根节点hostFiberNode
打上标记,最终只需要处理一次Placement
就可以将整个dom挂载到页面上。具体流程completeWork
会提到。 - 进入
reconcileChildrenFibers
后,执行reconcile
操作,根据传入的reactElement
的不同类型进入不同逻辑,目的都是根据reactElement
创建新的fiberNode
,根据具体操作打上副作用标记,然后连接到wip Fiber树
上。当然update时还要判断能不能复用current fiberNode
,这里涉及到diff算法
,根据单节点和多节点进入不同的逻辑,去判断更新前的哪些节点可以复用,不可复用的节点就要删除wip Fiber树
上与current fiberNode
对应的wip fiberNode
,当然这里没有删除逻辑,只是打上ChildDeletion
标记。 - 流程结束,给
wip fiberNode
生成了子 fiberNode
,然后进入子 fiberNode
的beginWork
流程,继续生成它的子fiberNode
。

completeWork
该过程是为了
- diff props 判断是否给传入的
fiberNode
打Update
标记 - flags冒泡:将它所有的
子fiberNode
的flags
冒泡到它身上,存到subtreeFlags
中。通过subtreeFlags
就可以快速得知该fiberNode
所在子树上是否存在副作用需要执行。 - 为
fiberNode
创建dom,初始化事件监听器或其他内部属性
具体来看,根据wip fiberNode.tag
的不同进入不同的逻辑,比如
HostRoot
、FunctionComponent
需要冒泡flagsHostComponent
、HostText
则需要创建dom或者diff props来判断是否需要打Update
标记
然后判断是进入sibling的beginWork
还是父节点的completeWork
commit
render 阶段完成后,fiberRootNode.finishedWork
就存着以hostRootFiber
为根节点并且带着flags的wip Fiber树
。
在commit阶段,会将各种副作用(flags)提交到宿主环境中。上面一直都没提React的调度功能,在Fiber架构运行过程中,render阶段是有可能会被高优先级任务打断的,但commit阶段一旦开始就会同步执行不会被打断。
整个阶段可以分为三个子阶段
before mutation
:执行dom操作前mutation
:执行dom操作layout
:执行dom操作后

before mutation
深度优先遍历wip Fiber树
,主要处理两种finerNode.tag
ClassComponent
,执行getSnapshotBeforeUpdate
HostRoot
,清空挂载的内容,方便mutation
阶段渲染
mutation
深度优先遍历wip Fiber树
,找到第一个不存在subtreeFlags
的节点,类似render阶段的流程,先向下遍历,找到之后再向上遍历(执行对应的操作),然后如果存在sibling的话,继续从sibling节点开始向下遍历,如此往复。
上面所说的操作,根据flags类型的不同而不同,有三种需要处理的类型
Placement
:插入或移动,找到 parent 对应的dom和找到 sibling 对应的dom,再找到当前fiberNode
对应的dom,然后插到parent的dom
中,当前fiberNode
对应的dom可能不止一个,也就是存在sibling,需要遍历插入。对于浏览器来说,会根据有没有sibling的dom
来判断使用appendChild
方法还是insertBefore
方法。由于fiberNode
不仅仅对应dom节点(HostComponent
),还有可能是函数组件等,所以寻找fiberNode
的parent、sibling、本身的对应的dom节点并不简单,尤其是寻找sibling,是一个比较耗时的操作。Update
:对于HostComponent
和HostText
来说可以处理style属性、innerHTML、直接文本节点还有其他属性的变化,FunctionComponent
还需要调用useLayoutEffect
的销毁函数。ChildDeletion
:所有要删除的子fiberNode
都保存在fiberNode.deletions
中,遍历deletions
执行操作,具体操作:- 每一个
fiberNode
在删除逻辑中都要走深度优先遍历的过程遍历子树,因为除了卸载dom,还有其他工作要做,比如解绑ref,函数组件useEffect
的销毁函数等。 - 找到当前要卸载
fiberNode
下的第一个dom,为了卸载。
- 每一个

layout
到这个阶段dom更新已经完成,但是JS线程还未结束,页面还没有渲染。这里也是深度优先遍历wip Fiber树
,根据fiberNode.tag
走不同的逻辑,但是每一个节点都会更新ref:
- 对于
ClassComponent
,执行componentDidMout/Update
,setState
传入的第二个参数callback存在fiberNode.updateQueue
中,这个时候调用。 - 对于
FunctionComponent
,执行useLayoutEffect
。 - 对于
HostRoot
,ReactDOM.render(element, container, callback)
传入的三个参数callback,这个时候调用。
这里也就可以知道,在上述所说调用的函数中可以访问到已经更新过的dom。
流程结束
到现在整个Fiber架构渲染的完整流程就结束了,当某个fiberNode
触发更新的时候,就会向上找到hostRootFiber
,然后开始render阶段再到commit阶段。
当然这只是一个渲染流程,还没有加入调度阶段。React实现了一套基于lane
模型的优先级算法,并在此基础上实现了批量更新、任务打断/恢复等特性。又基于这些特性实现了Concurrent Suspense
、useTransition
等开发者可以直接使用的内容。
把整个流程放在一个流程图中
立flag
这篇总结的内容是属于比较宏观的,很多细小的点都是一笔带过比如renconcile
算法、多节点和单节点的Fiber子树
的处理(能力也不足以支撑我写的太细😅),也由于调度系统还没有深入学习,所以本文没有提到调度相关内容。
后续有计划,着重于某些点写一下学习总结。除此之外,事件系统、Hooks架构等也都很值得总结。
转载自:https://juejin.cn/post/7211072055780573221