一文读懂 react Fiber
React Fiber 产生的原因
要知道React Fiber产生的原因是什么,首先我们得知道 React哲学,借用官网的话 React 是用 JavaScript 构建快速响应的大型 Web 应用程序的首选方式。快速响应是关键。那么制约网页快速响应的因素有哪些呢? 一般来说影响网页快速响应的有以下两类场景:
- 发送网络请求后,由于需要等待数据返回才能进一步操作导致不能快速响应。
- 当遇到大计算量的操作或者设备性能不足使页面掉帧,导致卡顿。
这两类场景可以概括为:
- CPU的瓶颈
- IO的瓶颈
React是怎么解决这两个瓶颈的呢?
CPU瓶颈
要解决 CPU 瓶颈,首先要明白什么是 “掉帧” 。我们知道主流浏览器的刷新频率为60HZ(1000ms/60HZ),即每16.6ms浏览器刷新一次。我们知道 JS 可以操作 DOM,GUI 渲染进程
与 JS 线程
是互拆的,所以JS脚本执行和浏览器布局、绘制不能同时执行。所以当一帧内js脚本执行占用过长时间(超过16.6ms),就没有时间去执行样式布局和样式绘制了,也就导致了所谓的掉帧
。明白了这个原理,我们就知道了解决问题的方法,那就是在浏览器每一帧的时间中,预留一些时间给JS线程,然后把控制权交还给渲染进程,让浏览器有剩余时间去执行样式布局和绘制。其中 React
预留的初始时间为5ms源码。当预留的时间不够用时,React
将线程控制权交还给浏览器使其有时间渲染UI,React
则等待下一帧时间到来继续被中断的工作。
这种将长任务拆分到每一帧中去执行每一段微小任务的操作被称为时间切片(time slice)
因此要解决 CPU 瓶颈
关键是要实现时间切片
,时间切片
的关键是将同步更新变为可中断的异步更新。
IO瓶颈
IO的瓶颈主要来源于网络延迟
,但很多情况下前端开发者是无法解决的,如何在开发者无法解决的前提下减少网络延迟
对用户的感知。
React
给出的答案是 将人机交互研究的结果整合到真实的 UI 中。为此 React
实现了 Suspense功能及配套的hook- useDeferredValue。为了实现这些特性,同样需要将同步更新变为可中断的异步更新。
在React 15
及以下版本中,React
使用了一种被称为 Stack Reconciler
的调度算法,当组件的更新任务被调度后,它会一直执行到更新任务完成,期间不允许其他的任务干扰。这种方式的优点是简单粗暴,但是也有明显的缺点,因为这会导致 UI 界面被卡死,失去了流畅性和响应性。因此这种更新方式已经不能满足需要,此时Fiber
就在16版本中应运而生了。
什么是 React Fiber
React Fiber
可以理解为一个执行单元(work unit)
,也可以说是一种新的数据结构,里面保存了保存了组件的 tag
、key
、type
、stateNode
等相关信息,用于表示组件树上的每个节点以及他们的关系。与传统的递归算法不同,在V16
版中 Reconciler
是基于 Fiber
节点实现的,被称为 Fiber Reconciler
,支持可中断异步更新,任务支持时间切片
。我们知道React
在数据更新时会有diff
的操作,此时diff
的过程是被分成一小段一小段的,Fiber节点保存了每一阶段任务的工作进度,js会比较一小部分虚拟dom,然后让出主线程,交给浏览器去做其他操作,然后继续比较,如此循环往复,直至完成diff
,然后一次性更新到视图上。
React Fiber 数据结构
从源码中我们可以看到Fiber
节点的定义Fiber节点定义如下:
function FiberNode(this: $FlowFixMe,tag: WorkTag,pendingProps: mixed,key:null | string,mode: TypeOfMode,) {
/**
Instance 静态数据结构属性
**/
this.tag = tag; // Fiber组件类型 Function/Class....
this.key = key; // key 属性,diff时需要
this.elementType = null; // 大部分情况同 type,某些情况不同,比如 FunctionComponent 使用 React.memo 包裹
this.type = null; // 对于 FunctionComponent,指函数本身,对于 ClassComponent,指 class,对于 HostComponent,指 DOM 节点的 tagName
this.stateNode = null; // Fiber 对应的真实 DOM 节点
/**
链接Fiber节点形成Fiber树所需属性
**/
this.return = null; // 指向父级Fbier节点
this.child = null; // 指向子Fiber节点
this.sibling = null; // 指向兄弟Fiber节点
this.index = 0; // 下标
this.ref = null;
this.refCleanup = null;
/**
Fiber 动态工作单元,保存本次更新相关信息
**/
this.pendingProps = pendingProps; // 当前组件属性
this.memoizedProps = null; // 指向最近一次使用props
this.updateQueue = null; // 更新队列
this.memoizedState = null; // 组件状态
this.dependencies = null; // 依赖组件数据
// 运行模式。React Fiber 支持两种模式,分别是 "concurrent"(并发模式)和 "legacy"(遗留模式)。
// 在并发模式下,React Fiber 会采取一系列优化策略,使组件的更新能够在多线程环境中异步地执行,从而提高应用的性能和用户体验。
// 而在遗留模式下,React Fiber 会采用与 React 16 及之前版本相同的同步更新方式,即组件更新会阻塞浏览器主线程的运行,直到更新完成后才能继续其他操作。
this.mode = mode;
/**
Effects 副作用相关
**/
this.flags = NoFlags; // 当前组件标记位,表示组件的操作类型和更新策略
this.subtreeFlags = NoFlags; // 当前组件子树的标记位
this.deletions = null; // 保存需要删除的fiber对象链表
/**
调度优先级
***/
this.lanes = NoLanes; // 当前组件的优先级
this.childLanes = NoLanes; // 子组件的优先级
this.alternate = null; // 指向该fiber在另一次更新时对应的fiber
}
fiber 工作原理
通过以上我们知道:
- 当组件树层级很深时,
React
会一次性遍历整颗组件树执行更新操作,导致性能瓶颈。 - 当前的调度策略是不可中断的,也就是说
React
执行中间无法打断。在大型应用中,如果执行任务时间太长,会导致页面出现卡顿现象,并影响用户体验。
React Fiber
通过分片(slicing)
和优先级调度(priority scheduling)
来解决上述问题,从而实现了高效的组件更新和异步渲染。React Fiber
的工作原理可以概括为以下几个步骤:
-
构建 Fiber 树
React Fiber
会创建一棵Fiber 树,
用于表示React
组件树的结构和状态。Fiber 树
是一个轻量级的树形结构,与React 组件树
一一对应。与传统的递归遍历不同,React Fiber
采用链表结构对树进行分片拆分,实现递增渲染的效果。 -
确定调度优先级
在
Fiber 树
构建完成后,React Fiber
会根据组件的更新状态和优先级,确定需要优先更新的组件,即“调度”更新。React Fiber
支持多个优先级,组件的优先级由组件的更新情况和所处的位置决定。比如页面的顶部和底部可以具有不同的优先级,用户的交互行为比自动更新的优先级更高,等等。 -
执行调度更新
当确定了需要调度更新的组件后,
React Fiber
会将这些组件标记为“脏”(dirty)
,并将它们放入更新队列中,待后续处理。需要注意的是,React Fiber
并未立即执行更新操作,而是等待时间片到来时才开始执行,这样可以让React Fiber
在执行更新时具有更高的优先级,提高了应用的响应性和性能。 -
中断和恢复
在执行更新时,如果需要中断当前任务,
React Fiber
可以根据当前任务的优先级、执行时间和剩余时间等因素,自动中断当前任务,并将现场保存到堆栈中。当下次处理到该任务的时候,React Fiber
可以通过恢复堆栈中保存的现场信息,继续执行任务,从而实现中断和恢复的效果。 -
渲染和提交
React Fiber
会将更新结果渲染到页面中,并设置下一次更新的时间和优先级。React Fiber
利用WebGL
和canvas
等浏览器原生的绘制API
,实现了GPU
加速,从而提高了渲染效率和性能。
此外,React
使用了一种叫做双缓存
的技术,何谓是双缓存
呢?双缓存
技术跟动画领域有关系,在计算机上的动画跟实际的动画不一样,实际的动画都是先画好了,播放的时候直接拿出来显示就行。计算机动画则是画一张,就拿出来一张,再画下一张,再拿出来。如果所需要绘制的图形很简单,那么这样也没什么问题。但一旦图形比较复杂,绘制需要的时间较长,问题就会变得突出。
举个例子,当我们用canvas
绘制动画,每一帧绘制前都会调用ctx.clearRect
清除上一帧的画面。
如果当前帧画面计算量比较大,导致清除上一帧画面到绘制当前帧画面之间有较长间隙,就会出现白屏。
为了解决这个问题,我们可以在内存中绘制当前帧动画,绘制完毕后直接用当前帧替换上一帧画面,由于省去了两帧替换间的计算时间,不会出现从白屏到出现画面的闪烁情况。这种在内存中构建并直接替换的技术叫做双缓存技术
。React
使用“双缓存”来完成Fiber树
的构建与替换——对应着DOM树
的创建与更新。
在React
中最多同时存在两颗Fiber树
,当前显示的Fiber树
称为current Fiber 树
,内存中构建的Fiber 树
,称为workInProgress Fiber树
。这两颗树中的Fiber
节点分别被称为current fiber
和 workInProgress fiber
,它们通过alternate
属性链接,每次状态更新都会产生新的 workInProgress Fiber
树。
currentFiber.alternate === workInProgressFiber;
workInProgressFiber.alternate === currentFiber;
React
通过使用current
指针在不同Fiber树
中的rootFiber
间切换来完成current Fiber树
的指向的变换。接下来我们用组件的mount/update
两个周期来展示创建/更新
流程。
用如下组件做演示:
const App = () => {
const [num, setNum] = useState(0);
return <div onClick={() => setNum(num + 1)}>{num}</div>
}
ReactDOM.render(<App/>, document.getElementById('root'));
-
Mount时
-
首次执行时:执行方法
ReactDOM.render
,创建fiberRoot
和rootFiber
,其中fiberRoot
为整个应用的根节点,rootFiber
为<App/>
所在组件树的根节点。一个应用可以多次调用ReactDOM.render
创建多个rootFiber
节点,但只有一个根节点,那就是fiberRoot
,当前current
指针指向当前fiber树
,示意图如下:fiberRoot.current = rootFiber
-
进入render阶段:
React
根据组件返回的jsx
在内存中依次创建fiber
节点并连接起来形成Fiber树,在内存中创建的Fiber树
为workInProgress树
,在此过程中React
会尝试复用current Fiber树
中已有的Fiber
节点中的属性。 -
随后进入commit阶段:右侧已经构建完的
workInProgress Fiber树
会替换掉当前的Fiber 树
,渲染到页面。此时fiberRoot
的current
指针指向workInProgress Fiber树
,使其成为current fiber树
。
-
-
Update
接下来我们来看看更新阶段时的具体过程:当我们点击
div
节点时触发状态改变,此时num
值从0
变为1
。此时React会开启新的render
过程并创建一颗新的workInProgress Fiber树
,此时workInProgress Fiber树
会尝试复用current Fiber树
对应节点的数据。当然是否复用其中就涉及到react diff
算法了。
此时render阶段完成后会进入commit阶段渲染到页面上,渲染完成后 workInProgress Fiber 树
变为current Fiber 树
。
Fiber
工作核心是双缓存技术,其创建和更新的过程伴随着DOM
的更新。
fiber 源码分析
前面我们说了React Fiber
的产生的原因、什么是Fiber
及工作原理。接下来我们我们从代码层面来看看Fiber
从创建到执行的过程。其经历两个阶段,分别是render阶段
及commit阶段
。
render阶段
首先会调用performSyncWorkOnRoot
和 performConcurrentWorkOnRoot
,同步更新调用performSyncWorkOnRoot
,异步更新调用performConcurrentWorkOnRoot
。
// performSyncWorkOnRoot
function workLoopSync() {
while (workInProgress !== null) {
performUnitOfWork(workInProgress);
}
}
// performConcurrentWorkOnRoot
function workLoopConcurrent() {
// Perform work until Scheduler asks us to yield
while (workInProgress !== null && !shouldYield()) {
// $FlowFixMe[incompatible-call] found when upgrading Flow
performUnitOfWork(workInProgress);
}
}
这两个方法的区别是否调用shouldYield
,表示如果当前帧没有空余时间,shouldYield
会终止循环,直至浏览器有空余时间再恢复遍历。两个方法都调用了performUnitOfWork
,接下来看看performUnitOfWork方法。
function performUnitOfWork(unitOfWork: Fiber): void {
const current = unitOfWork.alternate;
let next;
// 存在unitOfWork.child,不会处理sibling
if (enableProfilerTimer && (unitOfWork.mode & ProfileMode) !== NoMode) {
next = beginWork(current, unitOfWork, renderLanes);
} else {
next = beginWork(current, unitOfWork, renderLanes);
}
unitOfWork.memoizedProps = unitOfWork.pendingProps;
// 返回下一个待处理的fiber
if (next === null) {
// 执行归方法,收集副作用
completeUnitOfWork(unitOfWork);
} else {
workInProgress = next;
}
ReactCurrentOwner.current = null;
}
从这个方法方法中可以看出render阶段可以分为递
和归
两个阶段。递
阶段:首先从rootFiber节点开始向下深度优先遍历。每个fiber节点调用beginWork方法,该方法会根据传入的fiber
节点创建子fiber
节点并将他们链接起来。当遍历到该链路的叶子节点时(没有子组件),进入归
阶段:执行completeUnitOfWork方法,该方法调用completeWork处理fiber
节点。当某个Fiber节点
执行完completeUnitOfWork
,如果其存在兄弟Fiber节点
(即fiber.sibling !== null
),会进入其兄弟Fiber
的递
阶段。如果不存在兄弟Fiber
,会进入父级Fiber
的归
阶段。“递”和“归”阶段会交错执行直到“归”到rootFiber
。
下面我们举个例子,看上述流程:
function App() {
return (
<div>
<div>欢迎来到</div>
<span>jackbtone的博客</span>
</div>
)
}
ReactDOM.render(<App />, document.getElementById("root"));
对应的fiber节点结构如下
接下来我看看下在Render阶段
所涉及的几个关键方法。
- beginWork
function beginWork(current: Fiber | null,workInProgress: Fiber,renderLanes:Lanes,): Fiber | null {
const updateLanes = workInProgress.lanes; // 获取更新优先级
// 更新时复用上一个fiber节点的数据(优化性能)
if (current !== null) {
const oldProps = current.memoizedProps;
const newProps = workInProgress.pendingProps;
if (
oldProps !== newProps ||
hasLegacyContextChanged() ||
// 热加载重新渲染
(__DEV__ ? workInProgress.type !== current.type : false)
) {
// 上下文属性发生变化,给fiber节点打个标记
didReceiveUpdate = true;
} else if (!includesSomeLane(renderLanes, updateLanes)) {
// 两个节点相等,取消设置
didReceiveUpdate = false;
switch (workInProgress.tag) {
//....
}
// 复用current节点
return bailoutOnAlreadyFinishedWork(current, workInProgress, renderLanes);
} else {
if ((current.effectTag & ForceUpdateForLegacySuspense) !== NoEffect) {
didReceiveUpdate = true;
} else {
didReceiveUpdate = false;
}
}
} else {
didReceiveUpdate = false;
}
workInProgress.lanes = NoLanes;
// mount时根据不同类型tag创建不同的子Fiber节点
switch (workInProgress.tag) {
case IndeterminateComponent: {
// ...
}
case LazyComponent: {
// ...
}
// case.....
}
总结:该方法接受三个参数
1.current: 当前组件对应的fiber节点在上次更新时对应的fiber节点(workInProgress.alternate)
2.workInProgress:当前组件对应的fiber节点
3.renderLanes: 优先级相关参数。
由于组件初次渲染时 current === null,根据 fiber.tag类型不同,创建不同的子fiber节点。组件更新时 current !== null。此时可以复用 current 节点,current.child 作为 workInProgress.child。
其中具体 tag 类型可以点击这里查看。
- completeUnitOfWork
function completeUnitOfWork(unitOfWork: Fiber): void {
// 完成正在处理的工作单元, 然后转到下一个兄弟节点,如果没有兄弟节点则返回父节点Fiber
let completedWork: Fiber = unitOfWork;
do {
// 当前处理的Fiber节点
const current = completedWork.alternate;
// 父级 Fiber
const returnFiber = completedWork.return;
// 创建next
let next;
next = completeWork(current, completedWork, renderLanes);
if (next !== null) {
// 如果完成此 fiber 生成了新的工作,则继续处理该工作。
workInProgress = next;
return;
}
const siblingFiber = completedWork.sibling;
if (siblingFiber !== null) {
// 判断是否遍历兄弟节点组件树
workInProgress = siblingFiber;
return;
}
// 返回父节点Fiber
completedWork = returnFiber;
// Update the next thing we're working on in case something throws.
workInProgress = completedWork;
} while (completedWork !== null);
}
该段代码做了如下几件事
1. 创建下一个工作单元:将当前节点的 alternate 属性(用于存储 Fiber 节点的上一次状态)赋值给 current 变量,将当前节点的父级 Fiber 节点赋值给 returnFiber 变量,并调用 completeWork 函数来完成该节点的工作单元。
2. 处理下一个工作单元:如果当前节点的 completeWork 函数返回的是非空 Fiber 节点,则说明该节点生成了新的工作单元,需要继续处理
3. 返回父级节点:如果当前节点不存在子节点或者兄弟节点,则说明该工作单元已经被处理完毕,需要回溯到父级节点继续处理。为此,将 completedWork 变量指向上一级节点,重置 workInProgress 变量,并循环执行上述操作,直到所有的工作单元都被处理完成
- completeWork
function completeWork(
current: Fiber | null,
workInProgress: Fiber,
renderLanes: Lanes,
): Fiber | null {
const newProps = workInProgress.pendingProps;
// 不同tag类型处理逻辑不通
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: {
// ...
}
case HostRoot: {
// ...
}
case HostComponent: {
// ....
}
case HostText: {
// ...
}
// case....
}
}
该段代码主要做了如下几件事
1. 获取新属性值:从 workInProgress.pendingProps 属性中获取节点的新属性值 newProps。。
2. 根据节点类型处理逻辑:根据节点类型的不同,调用的函数也不同,但是它们的目的都是生成一个新的 Fiber 节点,用于标记该节点的更新情况。
3. 返回新的 Fiber 节点:在处理完节点之后,completeWork 函数会返回生成的新的 Fiber 节点。如果节点是被删除或者没有变更,则返回 null。
接下来我们分析页面渲染所用的hostComponent
类型,即原生DOM组件
所对应的fiber节点
。具体代码如下
case HostComponent: {
popHostContext(workInProgress);
const rootContainerInstance = getRootHostContainer();
const type = workInProgress.type;
if (current !== null && workInProgress.stateNode != null) {
// ...upadte 更新时
updateHostComponent(
current,
workInProgress,
type,
newProps,
rootContainerInstance,
);
// ...
} else {
// ... mount时
if (!newProps) {
// ...
return null;
}
// ...
}
return null;
}
可以看到completeWork
方法分为update
和mount
两种情况,判断依据也是根据 current === null
判断。同时对于hostComponent
,新加了判断条件workInProgress.stateNode !== null
(该fiber节点
是否存在对应的dom节点
)。
update
由于fiber节点已经存在对应的DOM节点,不需要生成新的DOM节点。主要是处理Prop数据:
- 注册回调函数
OnClick、onChange
等 - 处理
style prop
- 处理
children prop
其中主要是调用updateHostComponent
方法。
const updateHostComponent = function(
current: Fiber,
workInProgress: Fiber,
type: Type,
newProps: Props,
rootContainerInstance: Container,
) {
const oldProps = current.memoizedProps;
if (oldProps === newProps) {
return;
}
const instance: Instance = workInProgress.stateNode;
const currentHostContext = getHostContext();
const updatePayload = prepareUpdate(
instance,
type,
oldProps,
newProps,
rootContainerInstance,
currentHostContext,
);
workInProgress.updateQueue = (updatePayload: any);
if (updatePayload) {
markUpdate(workInProgress);
}
};
通过阅读源码可以发现,在updateHostComponent
内部,被处理完的props
会被赋值给workInProgress.updateQueue
,并最终会在commit阶段
被渲染在页面上。
mount
case HostComponent: {
popHostContext(workInProgress);
const rootContainerInstance = getRootHostContainer();
const type = workInProgress.type;
if (current !== null && workInProgress.stateNode != null) {
// update
if (current.ref !== workInProgress.ref) {
markRef(workInProgress);
}
} else {
// mount
const currentHostContext = getHostContext();
// 创建对应DOM节点
const instance = createInstance(
type,
newProps,
rootContainerInstance,
currentHostContext,
workInProgress,
);
// 将子孙DOM节点插入刚生成的DOM节点中
appendAllChildren(instance, workInProgress, false, false)
// DOM节点赋值给fiber.stateNode
workInProgress.stateNode = instance;
// 处理props数据
if (
finalizeInitialChildren(
instance,
type,
newProps,
rootContainerInstance,
currentHostContext,
)
) {
markUpdate(workInProgress);
}
}
通过阅读上述源码可以发现mount时做了以下事情:
- Fiber节点生成对应的DOM节点
- 将子孙DOM节点插入刚生成的DOM节点中
- 处理props数据
effectList
到此Render阶段
的大部分工作已经完成了,但需要注意的是在completeWork
的上层函数completeUnitOfWork
中,每个执行完completeWork
且存在effectTag
的Fiber节点
会被保存在一条被称为effectList
的单向链表中。
effectList
中第一个Fiber节点
保存在fiber.firstEffect
,最后一个元素保存在fiber.lastEffect
。在“归”阶段,所有有effectTag
的Fiber节点
都会被追加在effectList
中,最终形成一条以rootFiber.firstEffect
为起点的单向链表。源码如下:
if (returnFiber !== null &&
// 如果某个兄弟节点未完成,则不将效果附加到其父级
(returnFiber.effectTag & Incomplete) === NoEffect) {
// 将子树的所有操作和该 fiber 的操作附加到父级的effect list中。子级完成的顺序会影响副作用的顺序。
if (returnFiber.firstEffect === null) {
returnFiber.firstEffect = completedWork.firstEffect;
}
if (completedWork.lastEffect !== null) {
if (returnFiber.lastEffect !== null) {
returnFiber.lastEffect.nextEffect = completedWork.firstEffect;
}
returnFiber.lastEffect = completedWork.lastEffect;
}
// 如果此 fiber 具有副作用,则将其附加在子级的副作用之后。
// 如果需要,我们可以通过对效果列表进行多次传递来提前执行某些副作用。
// 我们不想在我们自己的列表上安排我们自己的副作用,因为如果我们最终重用了子级,
// 我们将在自己身上安排此副作用,因为我们处于列表的末尾。
const effectTag = completedWork.effectTag;
// 在创建 effect 列表时,跳过 NoWork 和 PerformedWork 标记。
if (effectTag > PerformedWork) {
if (returnFiber.lastEffect !== null) {
returnFiber.lastEffect.nextEffect = completedWork;
} else {
returnFiber.firstEffect = completedWork;
}
returnFiber.lastEffect = completedWork;
}
}
借用React
团队成员Dan Abramov的话:effectList
相较于Fiber树
,就像圣诞树上挂的那一串彩灯。
commit 阶段
进入commit阶段的函数是 commitRoot(root),其中root
传入的是fiberRootNode
,在rootFiber.firstEffect
上保存了一条需要执行副作用
的Fiber节点
的单向链表effectList
,这些Fiber节点
的updateQueue
中保存了变化的props
。这些副作用
对应的DOM操作
在commit
阶段执行。
commit
阶段也是分为三部分:
- before mutation阶段(执行
DOM
操作前) - mutation阶段(执行
DOM
操作) - layout阶段(执行
DOM
操作后)
主要逻辑如下:
function commitRoot(root) {
const renderPriorityLevel = getCurrentPriorityLevel();
runWithPriority(
ImmediateSchedulerPriority,
commitRootImpl.bind(null, root, renderPriorityLevel),
);
return null;
}
function commitRootImpl(root, renderPriorityLevel) {
do {
flushPassiveEffects();
} while (rootWithPendingPassiveEffects !== null);
if (firstEffect !== null) {
focusedInstanceHandle = prepareForCommit(root.containerInfo);
shouldFireAfterActiveInstanceBlur = false;
// before mutation阶段
commitBeforeMutationEffects(finishedWork);
// .....
// mutation阶段
commitMutationEffects(finishedWork, root, renderPriorityLevel);
//....
// layout阶段
commitLayoutEffects(finishedWork, root, lanes);
} else {
//.....
}
const rootDidHavePassiveEffects = rootDoesHavePassiveEffects;
return null;
}
从以上代码可以看出不同阶段代表着调用不同的方法,接下来分析不同阶段调用的具体方法。
before mutation
before mutation
就是遍历effectList
并调用 commitBeforeMutationEffects 函数处理。这里摘录了其中关键代码如下:
// 处理所有的 mutation effects,即所有的DOM操作,如删除、插入、替换等
function commitBeforeMutationEffects(firstChild: Fiber) {
let fiber = firstChild; // 初始化fiber变量
while (fiber !== null) {
if (fiber.deletions !== null) {
// 处理有待删除的节点
commitBeforeMutationEffectsDeletions(fiber.deletions);
}
// 有子节点的情况
if (fiber.child !== null) {
// 获取子树标记中 BeforeMutation 标志位的状态
const primarySubtreeTag = fiber.subtreeTag & BeforeMutation;
if (primarySubtreeTag !== NoSubtreeTag) {
// 递归调用 commitBeforeMutationEffects 函数处理子节点的 mutation effects。
commitBeforeMutationEffects(fiber.child);
}
}
if (__DEV__) {
// ......
} else {
try {
// 处理当前节点的 mutation effects
commitBeforeMutationEffectsImpl(fiber);
} catch (error) {
captureCommitPhaseError(fiber, error);
}
}
// 将fiber变量更新为下一个兄弟节点(fiber.sibling)
fiber = fiber.sibling;
}
}
其中 commitBeforeMutationEffects 方法如下:
function commitBeforeMutationEffectsImpl(fiber: Fiber) {
// 当前fiber节点及副作用类型
const current = fiber.alternate;
const effectTag = fiber.effectTag;
if (!shouldFireAfterActiveInstanceBlur && focusedInstanceHandle !== null) {
// 对焦点事件进行处理
}
// 执行 Snapshot 副作用。如果当前 fiber 节点存在 `Snapshot` 类型的副作用,则设置当前 debug fiber,并执行 `commitBeforeMutationEffectOnFiber` 函数,最后重置当前 debug fiber;
if ((effectTag & Snapshot) !== NoEffect) {
setCurrentDebugFiberInDEV(fiber);
// commitBeforeMutationEffectOnFiber 方法内部调用 getSnapshotBeforeUpdate,
commitBeforeMutationEffectOnFiber(current, fiber);
resetCurrentDebugFiberInDEV();
}
// 调度 useEffect,执行 Passive 副作用
if ((effectTag & Passive) !== NoEffect) {
if (!rootDoesHavePassiveEffects) {
// 设置根节点属性 rootDoesHavePassiveEffects 为true
rootDoesHavePassiveEffects = true;
scheduleCallback(NormalSchedulerPriority, () => {
// 调用 flushPassiveEffects 函数,将所有Passive类型的副作用进行批量处理
flushPassiveEffects();
return null;
});
}
}
}
这段代码的主要作用是在 React 中对异步渲染时的副作用进行处理,保证界面的正确性和性能。主要做了以下几件事:
- 处理
DOM节点
渲染/删除后的autoFocus
、blur
逻辑。 - 调用
getSnapshotBeforeUpdate
生命周期钩子。 - 调度
useEffect
。
调用 getSnapshotBeforeUpdate
commitBeforeMutationEffectOnFiber
是commitBeforeMutationLifeCycles
的别名。
在该方法内会调用 getSnapshotBeforeUpdate。
删减后主要代码如下:
function commitBeforeMutationLifeCycles(
current: Fiber | null,
finishedWork: Fiber,
): void {
// 通过不同类型的type执行不同操作
switch (finishedWork.tag) {
case FunctionComponent:
case ForwardRef:
case SimpleMemoComponent:
case Block: {
return;
}
// 处理类组件
case ClassComponent: {
// 判断是否需要执行 “Snapshot” 类型的副作用。通过调用instance.getSnapshotBeforeUpdate()获取组件在更新前的状态快照并记录
if (finishedWork.effectTag & Snapshot) {
// ......
const snapshot = instance.getSnapshotBeforeUpdate(
finishedWork.elementType === finishedWork.type
? prevProps
: resolveDefaultProps(finishedWork.type, prevProps),
prevState,
);
}
}
case HostRoot: {
if (supportsMutation) {
// 如果存在“Snapshot”类型的副作用,则清空根节点的容器信息
if (finishedWork.effectTag & Snapshot) {
const root = finishedWork.stateNode;
clearContainer(root.containerInfo);
}
}
return;
}
case HostComponent:
case HostText:
case HostPortal:
case IncompleteClassComponent:
// Nothing to do for these component types
return;
}
}
getSnapshotBeforeUpdate
生命周期的作用是返回要在更新后存储在实例上的值,以备更新后的组件实例中轻松恢复它们的状态。在该函数中,通常会处理组件的 DOM 元素,计算它们在更新前的位置、大小、样式等信息。只有 ClassComponent
类型的组件和 HostRoot
类型的根节点有可能会在更新前需要记录状态快照。其他类型的组件都不需要处理,可以直接返回。究其原因,是因为Stack Reconciler
重构为Fiber Reconciler
后,render阶段
的任务可能中断/重新开始,对应的组件在render阶段
的生命周期钩子(即componentWillXXX
)可能触发多次。
调度UseEffect
if ((effectTag & Passive) !== NoEffect) {
if (!rootDoesHavePassiveEffects) {
// 设置根节点属性 rootDoesHavePassiveEffects 为true
rootDoesHavePassiveEffects = true;
scheduleCallback(NormalSchedulerPriority, () => {
// 调用 flushPassiveEffects 函数,将所有Passive类型的副作用进行批量处理
flushPassiveEffects();
return null;
});
}
}
其中 scheduleCallback
是React
内部使用的一个调度器,用来调度任务的执行。由Scheduler
模块提供,该函数将 flushPassiveEffects
函数添加到处理队列中,并在浏览器空闲时执行。其中在在 flushPassiveEffects
方法内部会遍历rootWithPendingPassiveEffects
(即effectList
)执行effect
回调函数(其中effectList
中保存了需要执行副作用的Fiber节点
,包括节点的插入、更新、删除等)。
综上所述在 before mutation阶段
,会遍历effectList
,依次执行
- 处理
DOM节点
渲染/删除后的autoFocus
、blur
逻辑。 - 调用
getSnapshotBeforeUpdate
生命周期钩子。 - 调度
useEffect
。
mutation阶段
从上面可以知道在mutation阶段
阶段调用的是方法 commitMutationEffects,方法如下:
function commitMutationEffects(
firstChild: Fiber,
root: FiberRoot,
renderPriorityLevel,
) {
let fiber = firstChild;
// 遍历
while (fiber !== null) {
try {
commitMutationEffectsImpl(fiber, root, renderPriorityLevel);
} catch (error) {
// ......
}
}
fiber = fiber.sibling;
}
}
其中主要调用了方法 commitMutationEffectsImpl,接下来我们看看该方法做了什么事,代码如下:
function commitMutationEffectsImpl(
fiber: Fiber,
root: FiberRoot,
renderPriorityLevel,
) {
// 该方法第三个参数表示提交的 effect 的优先级
// 获取该fiber节点副作用类型
const effectTag = fiber.effectTag;
if (effectTag & ContentReset) {
// 根据 effectTag 和 ContentReset 将文本内容重置为初始值
commitResetTextContent(fiber);
}
// 更新ref
if (effectTag & Ref) {
const current = fiber.alternate;
if (current !== null) {
commitDetachRef(current);
}
if (enableScopeAPI) {
// TODO: This is a temporary solution that allows us to transition away
// from React Flare on www.
if (fiber.tag === ScopeComponent) {
commitAttachRef(fiber);
}
}
}
// 根据 effectTag 类型分别执行相应的操作
const primaryEffectTag = effectTag & (Placement | Update | Hydrating);
switch (primaryEffectTag) {
// 插入DOM
case Placement: {
commitPlacement(fiber);
// 移除 effectTag 类型
fiber.effectTag &= ~Placement;
break;
}
// 插入并更新 DOM
case PlacementAndUpdate: {
// Placement 插入
commitPlacement(fiber);
fiber.effectTag &= ~Placement;
// Update 更新
const current = fiber.alternate;
commitWork(current, fiber);
break;
}
// SSR相关
case Hydrating: {
fiber.effectTag &= ~Hydrating;
break;
}
// SSR相关
case HydratingAndUpdate: {
fiber.effectTag &= ~Hydrating;
// Update
const current = fiber.alternate;
commitWork(current, fiber);
break;
}
// 更新
case Update: {
const current = fiber.alternate;
commitWork(current, fiber);
break;
}
}
}
通过阅读源码我们可以知道commitMutationEffects
方法会遍历effectList
,并对每个Fiber
节点做以下三件事:
- 根据
ContentReset effectTag
重置文字节点 - 更新
ref
- 根据
effectTag
执行不同的操作。
可见在 mutation阶段
会遍历 effectList
,依次执行commitMutationEffects
。根据effectTag
调用不同的处理函数处理Fiber
。在以上执行完相应的操作之后,就完成了对于该 Fiber
的 Mutation Effects
的提交。值得注意的是,如果该 Fiber
的 effectTag
中仍包含任何一个主要标志(Placement、Update、Hydrating)
时,这部分标志在执行相应的操作之后都会被从effectTag
中移除,防止多次执行。
layout阶段
从上分析可知layout
阶段执行的函数是 commitLayoutEffects,其实也是遍历effectList
,执行一系列函数。
function commitLayoutEffects(
firstChild: Fiber,
root: FiberRoot,
committedLanes: Lanes,
) {
let fiber = firstChild;
while (fiber !== null) {
// 处理子节点
if (fiber.child !== null) {
// 子树标记和布局标记
const primarySubtreeTag = fiber.subtreeTag & Layout;
if (primarySubtreeTag !== NoSubtreeTag) {
commitLayoutEffects(fiber.child, root, committedLanes);
}
}
// .....
try {
// 执行布局effect
commitLayoutEffectsImpl(fiber, root, committedLanes);
} catch (error) {
captureCommitPhaseError(fiber, error);
}
fiber = fiber.sibling;
}
}
以上代码很简单,递归执行 commitLayoutEffects
方法,该方法内部调用了commitLayoutEffectsImpl
,源码如下:
function commitLayoutEffectsImpl(
fiber: Fiber,
root: FiberRoot,
committedLanes: Lanes,
) {
// 获取副作用类型
const effectTag = fiber.effectTag;
// 设置调试模式
setCurrentDebugFiberInDEV(fiber);
// 调用生命周期函数和hook,是否存在回掉函数和更新操作
if (effectTag & (Update | Callback)) {
const current = fiber.alternate;
// 提交布局
commitLayoutEffectOnFiber(root, current, fiber, committedLanes);
}
// enableScopeAPI 表示是否启用 Scope API
if (enableScopeAPI) {
if (effectTag & Ref && fiber.tag !== ScopeComponent) {
commitAttachRef(fiber);
}
} else {
// 赋值ref属性
if (effectTag & Ref) {
commitAttachRef(fiber);
}
}
resetCurrentDebugFiberInDEV();
}
从以上分析可知函数 commitLayoutEffectsImpl
,接受三个参数分别为fiber
、root
和committedLanes。
其中 committedLanes
表示提交优先级。其实就是做了两件事
- 调用
生命周期钩子
和hook
相关操作,实现函数为commitLayoutEffectOnFiber
。 - 赋值
ref
,其实现函数为commitAttachRef
。
commitLayoutEffectOnFiber 其实内部也是根据不同类型节点执行不同操作。commitAttachRef 则是获取dom
实例更新ref
,感兴趣的可以自己去阅读下,到此整个commit
阶段也就结束了。
fiber 总结
React Fiber
是 React 中的一个重要特性,它可以让 React 更高效地处理渲染任务和其他任务,并提高页面的性能和响应速度。React Fiber
的实现原理基于时间切片、任务调度和优先级管理等技术,通过分割任务、调度执行和管理状态等方式,实现了高效的组件渲染和更新流程。同时,React Fiber
还引入了Concurrent Mode、Suspense、Hooks
和函数组件等新特性,可以让我们更方便地管理组件的状态和生命周期函数,减少代码的冗余和复杂度。
参考文章
转载自:https://juejin.cn/post/7225957841319379005