一文读懂 react Fiber

2023-06-16
555219 分钟
...

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) ,也可以说是一种新的数据结构,里面保存了保存了组件的 tagkeytypestateNode 等相关信息,用于表示组件树上的每个节点以及他们的关系。与传统的递归算法不同,在V16版中 Reconciler是基于 Fiber 节点实现的,被称为 Fiber Reconciler,支持可中断异步更新,任务支持时间切片。我们知道React在数据更新时会有diff的操作,此时diff的过程是被分成一小段一小段的,Fiber节点保存了每一阶段任务的工作进度,js会比较一小部分虚拟dom,然后让出主线程,交给浏览器去做其他操作,然后继续比较,如此循环往复,直至完成diff,然后一次性更新到视图上。

React Fiber 数据结构

从源码中我们可以看到Fiber节点的定义Fiber节点定义如下:

function FiberNode(this: $FlowFixMe,tag: WorkTag,pendingProps: mixedkey: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 的工作原理可以概括为以下几个步骤:

  1. 构建 Fiber 树

    React Fiber 会创建一棵 Fiber 树,用于表示React组件树的结构和状态。Fiber 树是一个轻量级的树形结构,与 React 组件树一一对应。与传统的递归遍历不同,React Fiber 采用链表结构对树进行分片拆分,实现递增渲染的效果。

  2. 确定调度优先级

    Fiber 树构建完成后,React Fiber 会根据组件的更新状态和优先级,确定需要优先更新的组件,即“调度”更新。React Fiber 支持多个优先级,组件的优先级由组件的更新情况和所处的位置决定。比如页面的顶部和底部可以具有不同的优先级,用户的交互行为比自动更新的优先级更高,等等。

  3. 执行调度更新

    当确定了需要调度更新的组件后,React Fiber 会将这些组件标记为“脏”(dirty),并将它们放入更新队列中,待后续处理。需要注意的是,React Fiber 并未立即执行更新操作,而是等待时间片到来时才开始执行,这样可以让 React Fiber 在执行更新时具有更高的优先级,提高了应用的响应性和性能。

  4. 中断和恢复

    在执行更新时,如果需要中断当前任务,React Fiber 可以根据当前任务的优先级、执行时间和剩余时间等因素,自动中断当前任务,并将现场保存到堆栈中。当下次处理到该任务的时候,React Fiber 可以通过恢复堆栈中保存的现场信息,继续执行任务,从而实现中断和恢复的效果。

  5. 渲染和提交

    React Fiber 会将更新结果渲染到页面中,并设置下一次更新的时间和优先级。React Fiber 利用 WebGLcanvas 等浏览器原生的绘制 API,实现了GPU加速,从而提高了渲染效率和性能。

此外,React 使用了一种叫做双缓存的技术,何谓是双缓存呢?双缓存技术跟动画领域有关系,在计算机上的动画跟实际的动画不一样,实际的动画都是先画好了,播放的时候直接拿出来显示就行。计算机动画则是画一张,就拿出来一张,再画下一张,再拿出来。如果所需要绘制的图形很简单,那么这样也没什么问题。但一旦图形比较复杂,绘制需要的时间较长,问题就会变得突出。

举个例子,当我们用canvas绘制动画,每一帧绘制前都会调用ctx.clearRect清除上一帧的画面。 如果当前帧画面计算量比较大,导致清除上一帧画面到绘制当前帧画面之间有较长间隙,就会出现白屏。 为了解决这个问题,我们可以在内存中绘制当前帧动画,绘制完毕后直接用当前帧替换上一帧画面,由于省去了两帧替换间的计算时间,不会出现从白屏到出现画面的闪烁情况。这种在内存中构建并直接替换的技术叫做双缓存技术React使用“双缓存”来完成Fiber树的构建与替换——对应着DOM树的创建与更新。

React中最多同时存在两颗Fiber树,当前显示的Fiber树称为current Fiber 树,内存中构建的Fiber 树,称为workInProgress Fiber树。这两颗树中的Fiber节点分别被称为current fiberworkInProgress fiber,它们通过alternate属性链接,每次状态更新都会产生新的 workInProgress Fiber 树。

ini

复制代码

`currentFiber.alternate === workInProgressFiber;
workInProgressFiber.alternate === currentFiber;` 

React通过使用current指针在不同Fiber树中的rootFiber间切换来完成current Fiber树的指向的变换。接下来我们用组件的mount/update两个周期来展示创建/更新流程。 用如下组件做演示:

javascript

复制代码

 `const App = (

) => {
 const [num, setNum] = useState(0);
 return <div onClick={() => setNum(num + 1)}>{num}</div>
 }
 ReactDOM.render(<App/>, document.getElementById('root'));` 
  • Mount时

    • 首次执行时:执行方法ReactDOM.render,创建fiberRootrootFiber,其中fiberRoot为整个应用的根节点,rootFiber<App/>所在组件树的根节点。一个应用可以多次调用ReactDOM.render创建多个rootFiber节点,但只有一个根节点,那就是fiberRoot,当前current指针指向当前fiber树,示意图如下:

      image.png

      image.png

      ini
      
      复制代码
      
       `fiberRoot.current = rootFiber` 
    • 进入render阶段:React 根据组件返回的jsx在内存中依次创建fiber节点并连接起来形成Fiber树,在内存中创建的Fiber树workInProgress树,在此过程中React会尝试复用current Fiber树中已有的Fiber节点中的属性。

      image.png

      image.png

    • 随后进入commit阶段:右侧已经构建完的 workInProgress Fiber树会替换掉当前的 Fiber 树,渲染到页面。此时fiberRootcurrent指针指向workInProgress Fiber树,使其成为current fiber树

      image.png

      image.png

  • Update

    接下来我们来看看更新阶段时的具体过程:当我们点击div节点时触发状态改变,此时num值从0变为1。此时React会开启新的render过程并创建一颗新的workInProgress Fiber树,此时workInProgress Fiber树会尝试复用current Fiber树对应节点的数据。当然是否复用其中就涉及到react diff算法了。

image.png

image.png

此时render阶段完成后会进入commit阶段渲染到页面上,渲染完成后 workInProgress Fiber 树变为current Fiber 树

image.png

image.png

Fiber工作核心是双缓存技术,其创建和更新的过程伴随着DOM的更新。

fiber 源码分析

前面我们说了React Fiber的产生的原因、什么是Fiber及工作原理。接下来我们我们从代码层面来看看Fiber从创建到执行的过程。其经历两个阶段,分别是render阶段commit阶段

render阶段

首先会调用performSyncWorkOnRootperformConcurrentWorkOnRoot,同步更新调用performSyncWorkOnRoot,异步更新调用performConcurrentWorkOnRoot

scss

复制代码

 `// performSyncWorkOnRoot
 function workLoopSync() { 
 while (workInProgress !== null) {
 performUnitOfWork(workInProgress);
 }
}` 
scss

复制代码

 `//  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方法。

ini

复制代码

`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

下面我们举个例子,看上述流程:

javascript

复制代码

`function App(

) {
 return (
 <div>
 <div>欢迎来到</div>
 <span>jackbtone的博客</span>
 </div>
 )
}
ReactDOM.render(<App />, document.getElementById("root"));` 

对应的fiber节点结构如下

image.png

image.png

接下来我看看下在Render阶段所涉及的几个关键方法。

  • beginWork
php

复制代码

`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
ini

复制代码

`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);
}` 
markdown

复制代码

`该段代码做了如下几件事
1. 创建下一个工作单元:将当前节点的 alternate 属性(用于存储 Fiber 节点的上一次状态)赋值给 current 变量,将当前节点的父级 Fiber 节点赋值给 returnFiber 变量,并调用 completeWork 函数来完成该节点的工作单元。
2. 处理下一个工作单元:如果当前节点的 completeWork 函数返回的是非空 Fiber 节点,则说明该节点生成了新的工作单元,需要继续处理
3. 返回父级节点:如果当前节点不存在子节点或者兄弟节点,则说明该工作单元已经被处理完毕,需要回溯到父级节点继续处理。为此,将 completedWork 变量指向上一级节点,重置 workInProgress 变量,并循环执行上述操作,直到所有的工作单元都被处理完成` 
  • completeWork
php

复制代码

`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....
 }
}` 
php

复制代码

`该段代码主要做了如下几件事
1. 获取新属性值:从 workInProgress.pendingProps 属性中获取节点的新属性值 newProps。。
2. 根据节点类型处理逻辑:根据节点类型的不同,调用的函数也不同,但是它们的目的都是生成一个新的 Fiber 节点,用于标记该节点的更新情况。
3. 返回新的 Fiber 节点:在处理完节点之后,completeWork 函数会返回生成的新的 Fiber 节点。如果节点是被删除或者没有变更,则返回 null。` 

接下来我们分析页面渲染所用的hostComponent类型,即原生DOM组件所对应的fiber节点。具体代码如下

typescript

复制代码

`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方法分为updatemount两种情况,判断依据也是根据 current === null 判断。同时对于hostComponent,新加了判断条件workInProgress.stateNode !== null(该fiber节点是否存在对应的dom节点)。

update

由于fiber节点已经存在对应的DOM节点,不需要生成新的DOM节点。主要是处理Prop数据:

  • 注册回调函数OnClick、onChange
  • 处理 style prop
  • 处理 children prop

其中主要是调用updateHostComponent方法。

ini

复制代码

`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
scss

复制代码

`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且存在effectTagFiber节点会被保存在一条被称为effectList的单向链表中。 effectList中第一个Fiber节点保存在fiber.firstEffect,最后一个元素保存在fiber.lastEffect。在“归”阶段,所有有effectTagFiber节点都会被追加在effectList中,最终形成一条以rootFiber.firstEffect为起点的单向链表。源码如下:

ini

复制代码

 `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操作后)

主要逻辑如下:

scss

复制代码

`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 函数处理。这里摘录了其中关键代码如下:

scss

复制代码

`// 处理所有的 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 方法如下:

scss

复制代码

``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节点渲染/删除后的 autoFocusblur 逻辑。
  • 调用getSnapshotBeforeUpdate生命周期钩子。
  • 调度useEffect
调用 getSnapshotBeforeUpdate

commitBeforeMutationEffectOnFibercommitBeforeMutationLifeCycles的别名。 在该方法内会调用 getSnapshotBeforeUpdate。 删减后主要代码如下:

php

复制代码

`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
scss

复制代码

`if ((effectTag & Passive) !== NoEffect) {
 if (!rootDoesHavePassiveEffects) {
 // 设置根节点属性 rootDoesHavePassiveEffects 为true
 rootDoesHavePassiveEffects = true;
 scheduleCallback(NormalSchedulerPriority, () => {
 // 调用 flushPassiveEffects 函数,将所有Passive类型的副作用进行批量处理
 flushPassiveEffects();
 return null;
 });
 }
}` 

其中 scheduleCallbackReact内部使用的一个调度器,用来调度任务的执行。由Scheduler模块提供,该函数将 flushPassiveEffects 函数添加到处理队列中,并在浏览器空闲时执行。其中在在 flushPassiveEffects 方法内部会遍历rootWithPendingPassiveEffects(即effectList)执行effect回调函数(其中effectList中保存了需要执行副作用的Fiber节点,包括节点的插入、更新、删除等)。 综上所述在 before mutation阶段,会遍历effectList,依次执行

  • 处理DOM节点渲染/删除后的 autoFocusblur 逻辑。
  • 调用getSnapshotBeforeUpdate生命周期钩子。
  • 调度useEffect
mutation阶段

从上面可以知道在mutation阶段阶段调用的是方法 commitMutationEffects,方法如下:

javascript

复制代码

`function commitMutationEffects(

 firstChild: Fiber,
 root: FiberRoot,
 renderPriorityLevel,
) {
 let fiber = firstChild;
 // 遍历
 while (fiber !== null) {
 try {
 commitMutationEffectsImpl(fiber, root, renderPriorityLevel);
 } catch (error) {
 // ......
 }
 }
 fiber = fiber.sibling;
 }
}` 

其中主要调用了方法 commitMutationEffectsImpl,接下来我们看看该方法做了什么事,代码如下:

scss

复制代码

`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。在以上执行完相应的操作之后,就完成了对于该 FiberMutation Effects 的提交。值得注意的是,如果该 FibereffectTag 中仍包含任何一个主要标志(Placement、Update、Hydrating)时,这部分标志在执行相应的操作之后都会被从effectTag中��除,防止多次执行。

layout阶段

从上分析可知layout阶段执行的函数是 commitLayoutEffects,其实也是遍历effectList,执行一系列函数。

scss

复制代码

`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,源码如下:

scss

复制代码

`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,接受三个参数分别为fiberrootcommittedLanes。其中 committedLanes 表示提交优先级。其实就是做了两件事

  • 调用生命周期钩子hook相关操作,实现函数为commitLayoutEffectOnFiber
  • 赋值 ref,其实现函数为commitAttachRef

commitLayoutEffectOnFiber 其实内部也是根据不同类型节点执行不同操作。commitAttachRef 则是获取dom实例更新ref,感兴趣的可以自己去阅读下,到此整个commit阶段也就结束了。

fiber 总结

React Fiber 是 React 中的一个重要特性,它可以让 React 更高效地处理渲染任务和其他任务,并提高页面的性能和响应速度。React Fiber 的实现原理基于时间切片、任务调度和优先级管理等技术,通过分割任务、调度执行和管理状态等方式,实现了高效的组件渲染和更新流程。同时,React Fiber 还引入了Concurrent Mode、Suspense、Hooks和函数组件等新特性,可以让我们更方便地管理组件的状态和生命周期函数,减少代码的冗余和复杂度。

如果您觉得这篇文章有帮助,请点个赞吧~

分享文章

相关文章

更多文章 →
react2025-08-03
在 React 中实现倒计时功能会有什么坑
倒计时 倒计时是一个非常常见的业务场景,但是在 React 中实现起来,却不算简单。 首先我们来看这段倒计时代码,它能否正常执行? 来看看实际表现效果 可以看到计时器在不断执行,但是 的值却没有变。 这是一个很经典的问题:React 闭包陷阱 。 我们来详细分析下: 的依赖数组 是空的,这意味着 effect 只会在组件挂载时执行一次,而不会在 状态更新时重新执行。所以 回调函数中捕获的 值始终是初始值 。 这个问题解决起来也很简单,有...
学习面试
react2025-07-30
React性能优化三剑客:memo、useMemo和useCallback详解
前言 在React开发中,性能优化是一个永恒的话题。今天我们就来深入探讨React提供的三个重要性能优化工具: 、 和 ,它们如何帮助我们构建更高效的React应用。 1\. 为什么需要性能优化? 🤔 React的核心机制是当组件的state或props发生变化时,组件会重新渲染。但有时这种重新渲染是不必要的: 父组件更新导致所有子组件重新渲染 ,即使子组件的props没有变化 复杂计算在每次渲染时重复执行 ,消耗大量资源 函数引用在...
学习面试
react2024-12-06
React 19 终于发布,一大波新功能正式升级
这篇文章主要介绍了 React 19 的新功能,包括 Actions 自动处理数据突变相关状态,新增的 useActionState、useFormStatus、useOptimistic 等 hook,新的 use API 读取渲染资源,新的 React DOM 静态 API 生成静态站点,以及 React 服务器组件中的 RSC 和 RSA 等内容。同时提到后续会分享针对旧版功能的优化和改良。 关联问题: React 19 性能如何...
学习面试
react2024-09-18
React | 浏览器刷新,会执行 useEffect 的 卸载函数吗?
React | 浏览器刷新,会执行 useEffect 的 卸载函数吗? 需求描述 后端在跑一个长期的异步上传任务,在任务执行过程中,用户刷新页面(f5),此时产品希望能够发送一个 kill 任务,杀掉这个任务。 拆解 需求合理性 其他情况是否存在类似:比如关闭 tab,关闭浏览器,突然断网等操作,等浏览器层级或者异常的用户行为 前端是否应该响应这种行为:对于更深层次的需求处理 根本问题点,或者说为什么会存在这个需求:上一个任务会加锁,...
学习面试
react2024-07-29
Effect:渲染之外的副作用
React 借鉴了许多函数式编程的理念,使得它在构建复杂用户界面时能够保持代码的简洁、可维护和可测试性。函数式编程在很大程度上依赖于纯函数 纯函数(Pure Function)是指在计算机科学和函数式编程中使用的一类函数。一个函数如果具备以下两个主要特性,我们就可以称之为纯函数: 1. 引用透明性:纯函数在相同的输入下总是产生相同的输出。也就是说,函数的输出仅依赖于其输入参数,不依赖于任何外部状态或变量。 2. 无副作用:纯函数不会改变...
学习面试
react2023-06-16
说说你对 React diff的理解?原理是什么?
跟 一致, 通过引入 的概念,极大地避免无效的 操作,使我们的页面的构建效率提到了极大的提升 而 算法就是更高效地通过对比新旧 来找出真正的 变化之处 传统diff算法通过循环递归对节点进行依次对比,效率低下,算法复杂度达到 O(n^3), 将算法进行一个优化,复杂度降为 ,两者效率差距如下图: <img width="657" height="401" src="../../ resources/640 wx fmt png wxfr...
面试

评论

请登录后发表评论

去登录
加载评论中...

目录