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
和函数组件等新特性,能够让咱们更方便地办理组件的状况和生命周期函数,减少代码的冗余和复杂度。
参阅文章
- React 官网
- 知悉Fiber,方能百战不殆~
- React Fiber 源码解析