产生背景
在React16之前使用的虚拟DOM是数组的形式,又因为React本身是应用级框架,状态改变后并不能准确知道是哪个组件发生了改变,只能对整个应用进行diff协调,受限于虚拟DOM的结构,这个过程不能被打断,于是就会出现JS占用主线程过多时间,导致页面不能及时渲染而出现卡顿。
于是FaceBook团队对内部机制进行重构,旨在解决 React 在处理复杂和大型应用时面临的一些性能问题,尤其是渲染和更新界面的过程,fiber就诞生了。
核心概念
Fiber数据结构:Fiber本质上是一个JavaScript对象,代表React的一个工作单元,包含组件相关的信息,如类型、属性、状态等。
//本质上是一个对象结构
{
type: 'div', // 组件类型
key: null, // React key
props: { ... }, // 输入的props
state: { ... }, // 组件的state (如果是class组件或带有state的function组件)
child: Fiber | null, // 第一个子元素的Fiber
sibling: Fiber | null, // 下一个兄弟元素的Fiber
return: Fiber | null, // 父元素的Fiber
// ...其他属性
}
Fiber树:Fiber树是React Fiber架构中的一个核心概念,它是一种特殊的链表数据结构,用于表示React组件树的结构和状态。Fiber树的每个节点(Fiber节点)代表React中的一个工作单元,包含了组件的相关信息,如类型、属性、状态等,每个节点通过child、sibling和return字段构成一个链表结构,使React能够遍历组件树并知道从哪里开始、继续或停止工作,从而形成Fiber树。
//FiberNode结构
function FiberNode(
this: $FlowFixMe,
tag: WorkTag,
pendingProps: mixed,
key: null | string,
mode: TypeOfMode,
) {
// 基本属性
this.tag = tag; // 描述此Fiber的启动模式的值(LegacyRoot = 0; ConcurrentRoot = 1)
this.key = key; // React key
this.elementType = null; // 描述React元素的类型。例如,对于JSX<App />,elementType是App
this.type = null; // 组件类型
this.stateNode = null; // 对于类组件,这是类的实例;对于DOM元素,它是对应的DOM节点。
// Fiber链接
this.return = null; // 指向父Fiber
this.child = null; // 指向第一个子Fiber
this.sibling = null; // 指向其兄弟Fiber
this.index = 0; // 子Fiber中的索引位置
this.ref = null; // 如果组件上有ref属性,则该属性指向它
this.refCleanup = null; // 如果组件上的ref属性在更新中被删除或更改,此字段会用于追踪需要清理的旧ref
// Props & State
this.pendingProps = pendingProps; // 正在等待处理的新props
this.memoizedProps = null; // 上一次渲染时的props
this.updateQueue = null; // 一个队列,包含了该Fiber上的状态更新和副作用
this.memoizedState = null; // 上一次渲染时的state
this.dependencies = null; // 该Fiber订阅的上下文或其他资源的描述
// 工作模式
this.mode = mode; // 描述Fiber工作模式的标志(例如Concurrent模式、Blocking模式等)。
// Effects
this.flags = NoFlags; // 描述该Fiber发生的副作用的标志(十六进制的标识)
this.subtreeFlags = NoFlags; // 描述该Fiber子树中发生的副作用的标志(十六进制的标识)
this.deletions = null; // 在commit阶段要删除的子Fiber数组
this.lanes = NoLanes; // 与React的并发模式有关的调度概念。
this.childLanes = NoLanes; // 与React的并发模式有关的调度概念。
this.alternate = null; // Current Tree和Work-in-progress (WIP) Tree的互相指向对方tree里的对应单元
// 如果启用了性能分析
if (enableProfilerTimer) {
// ……
}
// 开发模式中
if (__DEV__) {
// ……
}
}
工作单元与任务调度:每个Fiber节点代表一个工作单元,React通过任务调度器根据任务的优先级来决定执行顺序,实现可中断和恢复的任务调度。
任务调度的实现:
调度主要涉及两个核心概念,优先级和控制权。
- 每个任务在调和的过程中,有自己的优先级,react之前的版本用expirationTime属性代表优先级,该优先级和IO不能很好的搭配工作(io的优先级高于cpu的优先级),现在有了更加细粒度的优先级表示方法Lane,Lane用二进制位表示优先级,二进制中的1表示位置,同一个二进制数可以有多个相同优先级的位,这就可以表示‘批’的概念,而且二进制方便计算。
- 在构建和更新用户界面时,React Fiber 会分批处理任务,并在每批任务之间让出控制权,以便浏览器可以执行其他操作。在 React
中,让出控制权的技术通常是通过使用浏览器的 requestIdleCallback 函数实现的,但是 react
考虑到兼容性问题并没有使用该 API,而是自己实现了一套机制,通过MessageChannel +
requestAnimationFrame自己模拟实现了requestIdleCallback。 - 具体过程:Scheduer 中通过 performance.now() 来获取 当前时间,在产生的一个任务对象中会记录该任务的开始时间(这个开始时间会根据任务优先级的不同添加不同的delay)和过期时间(这个过期时间是开始时间+timeout,不同的优先级有不同的timeout,过期时间小于开始时间时说明是立即执行),整个任务流水线在开始执行时也会记录一个全局的开始时间(每次任务中断后,在再次执行时,相当于开启了一个新的任务流水线,所以这个全局的开始时间会进行更新),当任务到了过期时间,并且没有到达需要浏览器渲染的时候就执行任务。
为什么不是setTimeout?
因为setTimeout的递归层级过深的话,延迟就不是1ms,而是4ms,这样会造成延迟时间过长
为什么不是requestAnimationFrame?
requestAnimationFrame是在微任务执行完之后,浏览器重排重绘之前执行,执行的时机是不准确的。如果raf之前JS的执行时间过长,依然会造成延迟。
与setTimeout相比,requestAnimationFrame最大的优势是由系统来决定回调函数的执行时机。(如果屏幕刷新率是60Hz,那么回调函数就每16.7ms被执行一次)
为什么不是requestIdleCallback?
requestIdleCallback的执行时机是在浏览器重排重绘之后,也就是浏览器的空闲时间执行。其实执行的时机依然是不准确的。
为什么是 MessageChannel?
首先,MessageChannel的执行时机比setTimeout靠前。其次,requestIdleCallback并不是所有浏览器都支持的。为了解决这个问题,React采用MessageChannel来模拟requestIdleCallback。
如何
如何判断有没有到达需要浏览器渲染的时候?
通过 shouldYieldToHost 方法来判断是否应该暂停任务流水线,归还主线程来进行渲染操作,shouldYieldToHost 中会获取当前时间,并减去全局的开始时间,如果这个差值大于了 Scheduer 设置的临界值(5ms),说明任务流水线执行时间有点长了,需要归还主线程,于是 shouldYieldToHost 返回 true。
工作原理
1.单元工作:每个Fiber节点代表一个单元,所有Fiber节点共同组成一个Fiber链表树(有链接属性,同时又有树的结构),这种结构让React可以细粒度控制节点的行为。
2.链接属性:child、sibling和return字段构成了Fiber之间的链接关系,使React能够遍历组件树并知道从哪里开始、继续或停止工作。
child指向子节点,sibing指向兄弟节点,return指向兄弟节点。
注:遍历顺序为ABCED
为什么选择链表?
Fiber 采用链表数据结构的原因是因为链表可以方便地在列表的中间插入和删除元素。这在构建和更新用户界面时非常有用,因为可能会有大量的元素需要插入或删除。
3.双缓冲技术:React在更新时,会根据现有的Fiber树(Current Tree)创建一个新的临时树(Work-in-progress (WIP) Tree),WIP-Tree包含了当前更新受影响的最高节点直至其所有子孙节点。Current Tree是当前显示在页面上的视图,WIP-Tree则是在后台进行更新,WIP-Tree更新完成后会复制其它节点,并最终替换掉Current Tree,成为新的Current Tree。因为React在更新时总是维护了两个Fiber树,所以可以随时进行比较、中断或恢复等操作,而且这种机制让React能够同时具备优秀的渲染性能和UI的稳定性。
Reconciler中在进行虚拟DOM的diff,它之所以可以中断,是因为fiber链表的这种结构,我们只要保存当前任务的指针就可以在下次直接找到该任务并执行,所以这种架构天然支持中断,并且 Fiber 双缓存 构建的两颗树中的 wip Fiber Tree 中保存着已经构建的虚拟DOM,当中断继续运行时根据保存的指针找到未完成的任务继续进行协调构建 wip Fiber Tree。
4.State和Props
props是组件的属性,是父组件传递给子组件的数据。在Fiber中,props被存储在Fiber节点的memoizedProps和pendingProps字段中。
- memoizedProps:表示上一次渲染时的props。这是在组件完成渲染后保存的props的副本。
- pendingProps:表示正在等待处理的新props。当组件接收到新的props时,这些props会被存储在pendingProps中,等待下一次渲染。
React通过比较memoizedProps和pendingProps来确定组件的props是否发生了变化。如果发生变化,React会触发组件的更新。
state是组件的内部状态,通常由组件自身管理。在Fiber中,state被存储在Fiber节点的memoizedState字段中。
- memoizedState:表示上一次渲染时的state。这是在组件完成渲染后保存的state的副本。
5.副作用的追踪:副作用的追踪是一个关键机制,它允许React在渲染过程中收集和管理需要执行的副作用操作。这些副作用操作包括DOM更新、生命周期方法调用等。
标记副作用:
每个Fiber节点都有一个flags字段和一个subtreeFlags字段,用于标识该节点及其子树中需要执行的副作用。flags字段直接标记该Fiber节点的副作用类型,而subtreeFlags字段则用于标记其子树中所有Fiber节点的副作用类型。这些副作用类型包括但不限于:
Placement:表示需要将一个DOM节点插入到DOM树中。
Update:表示需要更新一个DOM节点。
Deletion:表示需要从DOM树中删除一个DOM节点。
React 的所有 effect 类型都在这里 packages/shared/ReactSideEffectTags.js。
2.构建副作用链表
在React的渲染流程中,render阶段会从根节点开始处理所有的Fiber节点,收集有副作用的Fiber节点,并构建副作用链表。这个链表是通过Fiber节点的nextEffect指针连接而成的。在completeUnitOfWork阶段,每个Fiber节点会将自己的副作用链表提交给其父节点。具体步骤如下:
- 提交副作用链表:如果父节点没有副作用链表,则将父节点的firstEffect指向当前节点的firstEffect。如果当前节点有副作用链表,则将父节点的lastEffect的nextEffect指向当前节点的firstEffect。
- 添加当前节点到父节点的副作用链表:如果当前节点本身有副作用(即flags > 1),则将当前节点添加到父节点的副作用链表中。
3.执行副作用
在commit阶段,React会遍历副作用链表,并根据每个Fiber节点的flags标志执行对应的副作用操作。这个阶段是不可中断的,以确保所有副作用要么全部提交,要么全部不提交。具体操作包括:
- Placement:调用commitPlacement执行实际的插入操作,并清除Placement标记。
- Update:调用commitWork执行实际的更新操作,并清除Update标记。
- Deletion:调用commitDeletion执行实际的删除操作,并清除Deletion标记。
4.使用位操作
React使用位操作来处理副作用标记,因为位操作可以高效地进行集合操作。例如,通过按位或操作(|),可以将多个副作用标记合并到一个字段中;通过按位与操作(&),可以检查一个Fiber节点是否具有特定的副作用标记。
注:这也是Fiber架构更快的原因之一
工作流程
React Fiber的工作流程主要分为两个阶段:Reconciliation(调和)和Commit(提交)。
调和
调和的主要目标是在构建工作树的阶段,通过比较新的props和旧的Fiber树来确定哪些部分需要更新。
分三个阶段:
beginWork:创建和标记更新节点
1.1判断节点是否要更新
// packages/react-reconciler/src/ReactFiberBeginWork.js
function beginWork( current: Fiber | null, workInProgress: Fiber, renderExpirationTime: ExpirationTime,
): Fiber | null {
switch (workInProgress.tag) {
case ClassComponent: {
return updateClassComponent(current, workInProgress, Component, resolvedProps);
}
}
}
function updateClassComponent(current: Fiber | null, workInProgress: Fiber, Component: any, nextProps) {
const nextUnitOfWork = finishClassComponent(current, workInProgress, Component, shouldUpdate);
return nextUnitOfWork;
}
function finishClassComponent(
current: Fiber | null, workInProgress: Fiber, Component: any, shouldUpdate: boolean, hasContext: boolean
) {
return workInProgress.child;
}
beginWork 本身对递归没什么实际进展,主要是根据 tag 分发逻辑。
1.2 判断节点更新还是复用
function updateClassComponent(current, workInProgress, Component, nextProps) {
// 确定组件是否应该更新
const shouldUpdate = determineIfComponentShouldUpdate(
current,
workInProgress,
nextProps
);
// 传递正确的 shouldUpdate 参数
const nextUnitOfWork = finishClassComponent(
current,
workInProgress,
Component,
shouldUpdate,
false // 假设没有 context
);
return nextUnitOfWork;
}
function finishClassComponent(
current,
workInProgress,
Component,
shouldUpdate,
hasContext
) {
// 如果不需要更新,则复用当前子树
if (!shouldUpdate) {
cloneChildFibers(current, workInProgress);
return workInProgress.child;
}
// 继续正常的更新流程
return workInProgress.child;
}
function determineIfComponentShouldUpdate(current, workInProgress, nextProps) {
// 首次渲染时总是更新
if (current === null) {
return true;
}
// 浅比较 props 判断是否需要更新
const prevProps = current.memoizedProps;
// 检查 props 数量是否相同
if (Object.keys(prevProps).length !== Object.keys(nextProps).length) {
return true;
}
// 检查每个 prop 是否相同
for (let key in prevProps) {
if (prevProps[key] !== nextProps[key]) {
// 特殊处理 children
if (key === 'children') {
// 简化的 children 比较,实际可能需要更复杂的比较
if (prevProps.children !== nextProps.children) {
return true;
}
} else {
return true;
}
}
}
return false;
}
// 辅助函数:复用当前子树结构
function cloneChildFibers(current, workInProgress) {
// 简化实现,实际 React 中会更复杂
if (current.child) {
workInProgress.child = {
...current.child,
alternate: current.child,
return: workInProgress
};
// 递归克隆所有子节点
cloneChildren(current.child, workInProgress.child);
}
}
function cloneChildren(currentParent, wipParent) {
let currentChild = currentParent.child;
let wipChild = null;
let prevWipChild = null;
while (currentChild) {
const newChild = {
...currentChild,
alternate: currentChild,
return: wipParent,
sibling: null
};
if (!wipChild) {
wipParent.child = newChild;
wipChild = newChild;
} else {
prevWipChild.sibling = newChild;
}
prevWipChild = newChild;
currentChild = currentChild.sibling;
}
}
2.completeUnitOfWork和completeWork:收集副作用列表
2.1completeUnitOfWork 负责遍历Fiber节点,同时记录了有副作用节点的关系
// packages/react-reconciler/src/ReactFiberWorkLoop.js
function completeUnitOfWork(unitOfWork: Fiber): void {
let completedWork: Fiber = unitOfWork; // 当前正在完成的工作单元
do {
const current = completedWork.alternate; // 当前Fiber节点在另一棵树上的版本
const returnFiber = completedWork.return; // 当前Fiber节点的父节点
let next;
next = completeWork(current, completedWork, renderLanes); // 调用completeWork函数
if (next !== null) {
// 当前Fiber还有工作要完成
workInProgress = next;
return;
}
const siblingFiber = completedWork.sibling;
if (siblingFiber !== null) {
// 如果有兄弟节点,则进入兄弟节点的工作
workInProgress = siblingFiber;
return;
}
// 如果没有兄弟节点,回到父节点继续
completedWork = returnFiber;
workInProgress = completedWork;
} while (completedWork !== null);
// 如果处理了整个Fiber树,更新workInProgressRootExitStatus为RootCompleted,表示调和已完成
if (workInProgressRootExitStatus === RootInProgress) {
workInProgressRootExitStatus = RootCompleted;
}
}
需要注意的是,next 指针不应该重复经过同一个节点。因为如果向下的过程中经过某个节点,在向上的过程中又出现,就会再次进入 beginWork,造成死循环。
completeUnitOfWork 内部又创建了一层循环,搭配一个向上的新指针 workInProgress,然后循环看当前指针节点,有兄弟节点就返回交还给外层循环,没有就向上到父节点,直到最上面的根节点。
2.2 completeWork 在 completeUnitOfWork 中被调用,主要是根据 tag 进行不同的处理。
// packages/react-reconciler/src/ReactFiberCompleteWork.js
function completeWork(
current: Fiber | null,
workInProgress: Fiber,
renderLanes: Lanes,
): Fiber | null {
const newProps = workInProgress.pendingProps;
switch (workInProgress.tag) {
// 多种tag
case FunctionComponent:
case ForwardRef:
case SimpleMemoComponent:
bubbleProperties(workInProgress)
return null;
case ClassComponent:
// 省略逻辑
// ……
bubbleProperties(workInProgress)
return null;
case HostComponent:
// 省略逻辑
// ……
return null;
// 多种tag
// ……
}
}
从源码中可以看出来,completeWork起到一个分支处理的作用,在分支里,调用了bubbleProperties函数,这个函数主要用于记录Fiber的副作用标志和为子Fiber创建链表。
// packages/react-reconciler/src/ReactFiberCompleteWork.js
function bubbleProperties(completedWork: Fiber) {
const didBailout =
completedWork.alternate !== null &&
completedWork.alternate.child === completedWork.child; // 当前的Fiber与其alternate(备用/上一次的Fiber)有相同的子节点,则跳过更新
let newChildLanes = NoLanes; // 合并后的子Fiber的lanes
let subtreeFlags = NoFlags; // 子树的flags。
if (!didBailout) {
// 没有bailout,需要冒泡子Fiber的属性到父Fiber
let child = completedWork.child;
// 遍历子Fiber,并合并它们的lanes和flags
while (child !== null) {
newChildLanes = mergeLanes(
newChildLanes,
mergeLanes(child.lanes, child.childLanes),
);
subtreeFlags |= child.subtreeFlags;
subtreeFlags |= child.flags;
child.return = completedWork; // Fiber的return指向父Fiber,确保整个Fiber树的一致性
child = child.sibling;
}
completedWork.subtreeFlags |= subtreeFlags; // 合并所有flags(副作用)
} else {
// 有bailout,只冒泡那些具有“静态”生命周期的flags
let child = completedWork.child;
while (child !== null) {
newChildLanes = mergeLanes(
newChildLanes,
mergeLanes(child.lanes, child.childLanes),
);
subtreeFlags |= child.subtreeFlags & StaticMask; // 不同
subtreeFlags |= child.flags & StaticMask; // 不同
child.return = completedWork;
child = child.sibling;
}
completedWork.subtreeFlags |= subtreeFlags;
}
completedWork.childLanes = newChildLanes; // 获取所有子Fiber的lanes。
return didBailout;
}
调和过程可以被中断,那么在源码中是怎么实现的呢?
整体流程:
1.上下文准备:保存当前执行上下文和 dispatcher,设置渲染上下文
2.工作进度检查:判断是否需要为新渲染任务准备新的工作进度树
3.并发工作循环:持续处理工作单元,直到完成或被挂起
4.挂起处理:根据不同挂起原因(数据、错误、实例等)执行不同恢复策略
5.上下文恢复:重置执行上下文和 dispatcher
6.状态返回:根据渲染完成情况返回相应状态
// packages/react-reconciler/src/ReactFiberWorkLoop.js
// 以下只是核心逻辑的代码,不是renderRootConcurrent的完整源码
function renderRootConcurrent(root: FiberRoot, lanes: Lanes) {
// 保存当前的执行上下文和 dispatcher
const prevExecutionContext = executionContext;
executionContext |= RenderContext;
const prevDispatcher = pushDispatcher(root.containerInfo);
const prevCacheDispatcher = pushCacheDispatcher();
if (workInProgressRoot !== root || workInProgressRootRenderLanes !== lanes) {
// 如果当前的工作进度树与传入的 root 或 lanes 不匹配,我们需要为新的渲染任务准备一个新的堆栈。
// ……
}
// 持续的工作循环,除非中断发生,否则会一直尝试完成渲染工作
outer: do {
try {
if (
workInProgressSuspendedReason !== NotSuspended &&
workInProgress !== null
) {
// 如果当前的工作进度是由于某种原因而被挂起的,并且仍然有工作待处理,那么会处理它
const unitOfWork = workInProgress;
const thrownValue = workInProgressThrownValue;
// 根据不同挂起原因,进行中断、恢复等计算
resumeOrUnwind: switch (workInProgressSuspendedReason) {
case SuspendedOnError: {
// 如果工作因错误被挂起,那么工作会被中断,并从最后一个已知的稳定点继续
// ……省略逻辑
break;
}
case SuspendedOnData: {
// 工作因等待数据(通常是一个异步请求的结果)而被挂起,
// ……省略逻辑
break outer;
}
case SuspendedOnInstance: {
// 将挂起的原因更新为SuspendedOnInstanceAndReadyToContinue并中断工作循环,标记为稍后准备好继续执行
workInProgressSuspendedReason = SuspendedOnInstanceAndReadyToContinue;
break outer;
}
case SuspendedAndReadyToContinue: {
// 表示之前的挂起工作现在已经准备好继续执行
if (isThenableResolved(thenable)) {
// 如果已解析,这意味着需要的数据现在已经可用
workInProgressSuspendedReason = NotSuspended;
workInProgressThrownValue = null;
replaySuspendedUnitOfWork(unitOfWork); // 恢复执行被挂起的工作
} else {
workInProgressSuspendedReason = NotSuspended;
workInProgressThrownValue = null;
throwAndUnwindWorkLoop(unitOfWork, thrownValue); // 继续循环
}
break;
}
case SuspendedOnInstanceAndReadyToContinue: {
// ……省略部分逻辑
const isReady = preloadInstance(type, props);
if (isReady) {
// 实例已经准备好
workInProgressSuspendedReason = NotSuspended; // 该fiber已完成,不需要再挂起
workInProgressThrownValue = null;
const sibling = hostFiber.sibling;
if (sibling !== null) {
workInProgress = sibling; // 有兄弟节点,开始处理兄弟节点
} else {
// 没有兄弟节点,回到父节点
const returnFiber = hostFiber.return;
if (returnFiber !== null) {
workInProgress = returnFiber;
completeUnitOfWork(returnFiber); // 收集副作用,前面有详细介绍
} else {
workInProgress = null;
}
}
break resumeOrUnwind;
}
}
// 还有其它case
}
}
workLoopConcurrent(); // 如果没有任何工作被挂起,那么就会继续处理工作循环。
break;
} catch (thrownValue) {
handleThrow(root, thrownValue);
}
} while (true);
// 重置了之前保存的执行上下文和dispatcher,确保后续的代码不会受到这个函数的影响
resetContextDependencies();
popDispatcher(prevDispatcher);
popCacheDispatcher(prevCacheDispatcher);
executionContext = prevExecutionContext;
// 检查调和是否已完成
if (workInProgress !== null) {
// 未完成
return RootInProgress; // 返回一个状态值,表示还有未完成
} else {
// 已完成
workInProgressRoot = null; // 重置root
workInProgressRootRenderLanes = NoLanes; // 重置Lane
finishQueueingConcurrentUpdates(); // 处理队列中的并发更新
return workInProgressRootExitStatus; // 返回当前渲染root的最终退出状态
}
}
提交
提交是遍历在Reconciliation阶段创建的副作用列表进行更新DOM并执行任何副作用。
主要分三个阶段:
- BeforeMutation:遍历副作用列表
// packages/react-reconciler/src/ReactFiberCommitWork.js
export function commitBeforeMutationEffects(
root: FiberRoot,
firstChild: Fiber,
): boolean {
nextEffect = firstChild; // nextEffect是遍历此链表时的当前fiber
commitBeforeMutationEffects_begin(); // 遍历fiber,处理节点删除和确认节点在before mutation阶段是否有要处理的副作用
const shouldFire = shouldFireAfterActiveInstanceBlur; // 当一个焦点元素被删除或隐藏时,它会被设置为 true
shouldFireAfterActiveInstanceBlur = false;
focusedInstanceHandle = null;
return shouldFire;
}
这个没什么可说的,就是对副作用进行遍历,调用 getSnapshotBeforeUpdate 等生命周期方法。
- CommitMutation:正式提交
// packages/react-reconciler/src/ReactFiberCommitWork.js
export function commitMutationEffects(
root: FiberRoot,
finishedWork: Fiber,
committedLanes: Lanes,
) {
// lanes和root被设置为"in progress"状态,表示它们正在被处理
inProgressLanes = committedLanes;
inProgressRoot = root;
// 递归遍历Fiber,更新副作用节点
commitMutationEffectsOnFiber(finishedWork, root, committedLanes);
// 重置进行中的lanes和root
inProgressLanes = null;
inProgressRoot = null;
}
commitMutationEffects 函数是 React 提交阶段的入口点,主要执行以下操作:
- 状态准备:设置全局变量标记当前正在处理的 root 和 lanes
- 副作用执行:递归遍历 Fiber 树,执行所有需要提交的副作用(DOM 更新、生命周期调用等)
- 状态重置:清除全局标记,完成提交阶段
3.commitLayout:处理layout effects
// packages/react-reconciler/src/ReactFiberCommitWork.js
export function commitLayoutEffects(
finishedWork: Fiber,
root: FiberRoot,
committedLanes: Lanes,
): void {
inProgressLanes = committedLanes;
inProgressRoot = root;
// 创建一个current指向就Fiber树的alternate
const current = finishedWork.alternate;
// 处理那些由useLayoutEffect创建的layout effects
commitLayoutEffectOnFiber(root, current, finishedWork, committedLanes);
inProgressLanes = null;
inProgressRoot = null;
}
提交阶段三个子阶段的核心区别
阶段 | 执行时机 | 核心操作 | 示例操作 |
---|---|---|---|
Before Mutation | DOM 更新前 | 读取 DOM 状态(如 scroll 位置) | getSnapshotBeforeUpdate 、调度 useEffect |
Mutation | DOM 更新中 | 执行 DOM 插入 / 更新 / 删除操作 | commitPlacement 、commitUpdate 、commitDeletion |
Layout | DOM 更新后,渲染前 | 同步执行依赖 DOM 的副作用、调用生命周期方法 | componentDidMount 、componentDidUpdate 、useLayoutEffect 的回调函数 |
提交之后就无法中断了。
优势特点
提升性能:通过可中断的渲染和增量渲染,避免了长时间占用主线程资源,减少了卡顿现象,提高了应用的性能。
优化用户体验:能够根据任务优先级进行调度,确保高优先级任务得到及时处理,提升了应用的交互体验。
支持新特性:为React引入一些新特性提供了基础,如异步渲染、懒加载等。