Fiber为何被提出?
在React的组件更新时,对于计算机而言,它往往是一种CPU密集的操作,因为它要通过对比新旧虚拟DOM树,从而找出需要更新的内容,并通过打补丁的方式去将更新映射到真实的DOM树上。当页面的构成比较简单时,这个过程通常来说不会有太大的负担。但是当要对比的组件树非常多时,就会发生大量的新旧节点对比,CPU的压力也会变得愈加庞大,从而拉长了整个更新的时间,当时间超过16.6ms也就是一帧的时间时,用户往往就会感觉到明显的卡顿感。
在React 16之前,页面的整体更新操作,都是同步且不可中断的。换言之,当用户触发了某个页面更新的操作,到页面更新完成之前,整个页面都是完全被JS线程所阻塞的,做不了其他的任何事情。因为一旦中断,调用栈就会被销毁,中间的状态就丢失了。这种基于调用栈的实现,我们称为 Stack Reconcilation。
而在React 16之后,React采用了全新的Fiber架构作为组件的最小执行单元,并在架构层上新增了Scheduler(调度器)。通俗的来说,React将更新组件时大量的CPU计算的工作,利用"时间分片"的方案,将原本要一次性做的工作,拆分成一个个异步任务,在浏览器空闲的时间时执行。这种新的架构称为 Fiber Reconcilation。
在 React 中,Fiber 模拟之前的递归调用,具体通过链表的方式去模拟函数的调用栈,并且保存了父节点、兄弟节点、子节点的节点信息,这样就可以做到中断调用,将一个大的更新任务,拆分成小的任务,并设置优先级,在浏览器空闲的时异步执行。
这样一来,当浏览器存在优先级更高的任务,或者当前的更新操作耗时过长时,React可以主动的将线程权交还给浏览器去做优先级更高的任务或者绘制工作,等到浏览器空闲时,再回过头来完成被中断的更新操作。
Fiber是什么?
那么,Fiber是什么呢?
在书写React代码时,我们通常会使用JSX去描述页面的每一个节点。而等到代码执行时,React runtime会将我们书写的JSX代码,转化为Render Function,执行Render Function之后,会得到一个个对应的React Element,最终再通过React Element转化为最终的Fiber树结构。
可以说,Fiber在React里是一个个最小的工作单元,同时也保存着我们页面上每一个节点的对应信息。
js
function FiberNode(
tag: WorkTag,
pendingProps: mixed,
key: null | string,
mode: TypeOfMode,
) {
// Instance
this.tag = tag; // 组件类型,比如 Function/Class/Host
this.key = key; // key 唯一值,通常会在列表中使用
this.elementType = null;
this.type = null; // 元素类型,字符串或类或函数,比如 "div"/ComponentFn/Class
this.stateNode = null; // 指向真实 DOM 对象
// Fiber
this.return = null; // 父 Fiber
this.child = null; // 子 Fiber 的第一个
this.sibling = null; // 下一个兄弟节点
this.index = 0; // 在同级兄弟节点中的位置
this.ref = null;
this.pendingProps = pendingProps;
this.memoizedProps = null;
this.updateQueue = null;
this.memoizedState = null;
this.dependencies = null;
this.mode = mode;
// Effects
this.flags = NoFlags;
this.subtreeFlags = NoFlags;
this.deletions = null;
this.lanes = NoLanes;
this.childLanes = NoLanes;
this.alternate = null;
// ...
}
为了保证任务的可中断和可恢复性,Fiber会保存着每个节点对应的父节点、兄弟节点和子节点的信息, 例如:
javascript
function App() {
return (
<div className="app">
<span>hello</span>, Fiber
</div>
);
}
则对应的Fiber树为:

Fiber树是如何被构建的?
在React的整个渲染过程中,总体可以分为两个阶段:
- render
- commit
render阶段
开始于performSyncWorkOnRoot
或performConcurrentWorkOnRoot
方法的调用。这取决于本次更新是同步更新还是异步更新,所谓的异步更新实则就是会根据浏览器是否存在空闲时间,来动态调整是否打断当前这一批次的更新工作,也就是concurrent模式。在React18中会默认开启。
scss
// performSyncWorkOnRoot会调用该方法
function workLoopSync() {
while (workInProgress !== null) {
performUnitOfWork(workInProgress);
}
}
// performConcurrentWorkOnRoot会调用该方法
function workLoopConcurrent() {
while (workInProgress !== null && !shouldYield()) {
performUnitOfWork(workInProgress);
}
}
而代码里的performUnitOfWork方法则是创建Fiber节点的起点,他里面所做的主要工作可以分为两部分:
- beginwork
- completework
具体两部分工作做了什么呢?
在beginwork的过程中,React会传入当前的Fiber节点,通过条件判断当前Fiber节点是否能够复用,如果能够复用则直接使用,如果不能则利用diff算法,创建出新的Fiber节点。
php
function beginWork(
current: Fiber | null,
workInProgress: Fiber,
renderLanes: Lanes,
): Fiber | null {
// ...
}
-
current:当前组件对应的
Fiber节点
在上一次更新时的Fiber节点
。 -
workInProgress:当前组件对应的
Fiber节点
-
renderLanes:优先级
-
update
时:如果current
存在,在满足一定条件时可以复用current
节点,这样就能克隆current.child
作为workInProgress.child
,而不需要新建workInProgress.child
。 -
mount
时:除fiberRootNode
以外,current === null
。会根据fiber.tag
不同,创建不同类型的子Fiber节点
php
function beginWork(
current: Fiber | null,
workInProgress: Fiber,
renderLanes: Lanes
): Fiber | null {
// update时:如果current存在可能存在优化路径,可以复用current(即上一次更新的Fiber节点)
if (current !== null) {
// ...省略
// 复用current
return bailoutOnAlreadyFinishedWork(
current,
workInProgress,
renderLanes,
);
} else {
didReceiveUpdate = false;
}
// mount时:根据tag不同,创建不同的子Fiber节点
switch (workInProgress.tag) {
case IndeterminateComponent:
// ...省略
case LazyComponent:
// ...省略
case FunctionComponent:
// ...省略
case ClassComponent:
// ...省略
case HostRoot:
// ...省略
case HostComponent:
// ...省略
case HostText:
// ...省略
// ...省略其他类型
}
}
类似beginWork
,completeWork
也是针对不同fiber.tag
调用不同的处理逻辑。
php
function completeWork(
current: Fiber | null,
workInProgress: Fiber,
renderLanes: Lanes,
): Fiber | null {
const newProps = workInProgress.pendingProps;
switch (workInProgress.tag) {
case IndeterminateComponent:
case LazyComponent:
case SimpleMemoComponent:
case FunctionComponent:
case ForwardRef:
case Fragment:
case Mode:
case Profiler:
case ContextConsumer:
case MemoComponent:
return null;
case ClassComponent: {
// ...省略
return null;
}
case HostRoot: {
// ...省略
updateHostContainer(workInProgress);
return null;
}
case HostComponent: {
// ...省略
return null;
}
// ...省略
mount
时的主要逻辑包括三个:
- 为
Fiber节点
生成对应的DOM节点
- 将子孙
DOM节点
插入刚生成的DOM节点
中 - 与
update
逻辑中的updateHostComponent
类似的处理props
的过程
scss
const currentHostContext = getHostContext();
// 为fiber创建对应DOM节点
const instance = createInstance(
type,
newProps,
rootContainerInstance,
currentHostContext,
workInProgress,
);
// 将子孙DOM节点插入刚生成的DOM节点中
appendAllChildren(instance, workInProgress, false, false);
// DOM节点赋值给fiber.stateNode
workInProgress.stateNode = instance;
// 与update逻辑中的updateHostComponent类似的处理props的过程
if (
finalizeInitialChildren(
instance,
type,
newProps,
rootContainerInstance,
currentHostContext,
)
) {
markUpdate(workInProgress);
}