0. 前言
这篇文章是《React技术揭秘》学习笔记系列文章的第二篇文章。在这篇文章中,我们继续跟随这本书的脚步,学习React16的Fiber架构的工作流程。前文指路:
- 《React技术揭秘》学习笔记(一):理念篇(上)
- 《React技术揭秘》学习笔记(一):理念篇(下) React16之后的版本,利用Fiber架构更新和渲染页面的过程,分为render和commit两个阶段。而render阶段采用递归的方式进行工作,分为两个过程:“递”的过程和“归”的过程;commit阶段分为三个子阶段:before mutation阶段、mutation阶段,以及layout阶段,除此之外,在before mutation阶段之前还有一些准备工作,在layout阶段之后还有一些收尾工作。
1. render阶段
render阶段开始于preformSyncWorkOnRoot或performConcurrentQorkOnRoot方法的调用。这取决于本次更新是同步更新还是异步更新。相关代码如下:
// performSyncWorkOnRoot会调用该方法
function workLoopSync() {
while (workInProgress !== null) {
performUnitOfWork(workInProgress);
}
}
// performConcurrentWorkOnRoot会调用该方法
function workLoopConcurrent() {
while (workInProgress !== null && !shouldYield()) {
performUnitOfWork(workInProgress);
}
}
容易发现,他们的区别是是否调用shouldYield()。该方法用来判断当前浏览器帧是否有剩余时间,如果没有剩余时间,shouldYield终止循环,直到浏览器有空闲时间后再继续遍历。
workInProgress代表当前创建的workInProgress fiber.performUnitOfWork方法会创建下一个Fiber节点并赋值给workInProgress.然后将已创建的Fiber节点连接成Fiber树。performUnitOfWork是Fiber架构的主要方法,它的工作可以分为两个部分:“递”和“归”。
1.1 “递”的过程
“递”的阶段会执行beginWork.首先,我们来根据原文中的一张流程图来看一看beginWork是如何工作的。
如图所示,beginWork这个方法会传入两个关键的参数:current和workInProgress,分别对应当前组件对应的Fiber节点在上一次更新时的Fiber节点 (即workInProgress.alternate) 和当前组件对应的Fiber节点。除此之外,beginWork,还有一个参数,叫renderLanes,该参数与优先级相关,和本节内容无关,因此我们选择性的将它忽略掉。
顺着流程图往下看,可以看到beginWork的第一步是根据current是否为空来判断当前是mount阶段还是update阶段。我们知道,如果是首屏渲染,则执行mount阶段,否则执行update阶段。如果当前是第一次创建Fiber节点,则一定不会有上一次更新,对应的current也一定会为null。因此,我们可以通过判断current === null来选择mount阶段和update阶段。
1.1.1 mount阶段
在mount阶段,根据workInProgress.tag判断需要创建的Fiber节点类型,并且创建相应的Fiber节点。我们可以从这里看到tag的所有类型。对于我们常见的组件类型,如FuctionComponent,ClassComponent,HostComponent,最终会进入reconcileChildren方法。
1.1.2 update阶段
顺着另一条线路来看看update阶段。在该阶段,首先通过对传入的两个Fiber节点的type和props进行比较,判断该节点是否可以复用。若不可以复用,则把他当成新的Fiber节点看待,走类似mount阶段的流程,最后生成带effecTag的新的Fiber节点;若可以复用,则检查子树是否需要更新。若子树需要更新,则执行cloneChildFibers并且返回子节点;否则直接返回null.
1.1.3 reconcileChildren
从该函数名就能看出这是Reconciler模块的核心部分。它对于从不同路径来的Fiber节点做了不同的事情:
- 对于
mount组件,他会创建新的Fiber节点 - 对于
update组件,他会将当前组件于该节点上次更新时对应的Fiber节点(Diff算法)进行比较,将比较的结果生成新的Fiber节点
export function reconcileChildren(
current: Fiber | null,
workInProgress: Fiber,
nextChildren: any,
renderLanes: Lanes
) {
if (current === null) {
// 对于mount的组件
workInProgress.child = mountChildFibers(
workInProgress,
null,
nextChildren,
renderLanes,
);
} else {
// 对于update的组件
workInProgress.child = reconcileChildFibers(
workInProgress,
current.child,
nextChildren,
renderLanes,
);
}
}
这里可以看出,他也是通过current === null?来区分mount和update的。并且我们发现在流程图中的mountChildFibers和reconcileChildFibers这两个方法的实际执行者都是reconcileChildren.
但无论走哪个逻辑,最终它都会生成新的子Fiber节点,并赋值给workInprogress.child,作为本次beginWork的返回值,并且作为下次perfomUnitOfWork执行时workInProgress的传参。
1.1.4 effectTag
我们知道,render阶段的工作是在内存中进行,当工作结束后会通知Renderer需要执行的DOM操作。要执行DOM操作的具体类型就保存在fiber.effectTag中
1.2 “归”的过程
归的过程会执行compleWork。类似beginWork,completeWork也是针对不同fiber.tag调用不同的处理逻辑。我们首先来看流程图:
同样,completeWork传入三个参数:current、workInProgress和renderLanes。它首先根据workInprogress.tag判断组件的类型,并且把它们分开处理。我们在这里以HostComponent为例来说明。
和beginWork一样,它也是通过判断current === null?来判断mount阶段和update阶段。
1.2.1 mount阶段
mout时的主要逻辑有三个:
- 为Fiber节点生成对应的DOM节点
- 将子孙DOM节点插入到刚生成的DOM节点中
- 处理
props
// mount的情况
// ...省略服务端渲染相关逻辑
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);
}
由于completeWork属于“归”阶段调用的函数,每次调用appendAllChildren时都会将已生成的子孙DOM节点插入当前生成的DOM节点下。那么,当“归”到rootFiber时,我么已经有一个构建好的离屏DOM树。
1.2.2 update阶段
在update阶段,Fiber节点已经存在对应的DOM节点,所以不需要生成DOM节点。需要做的就是处理props
if (current !== null && workInProgress.stateNode != null) {
// update的情况
updateHostComponent(
current,
workInProgress,
type,
newProps,
rootContainerInstance,
);
}
你可以从这里看到updateHostComponent方法定义。
在updateHostComponent内部,被处理完的props会被赋值给workInProgress.updateQueue,并最终会在commit阶段被渲染在页面上。
workInProgress.updateQueue = (updatePayload: any);
其中updatePayload为数组形式,他的偶数索引的值为变化的prop key,奇数索引的值为变化的prop value。
1.2.3 effectList
one more question:作为DOM操作的依据,commit阶段需要找到所有有effectTag的Fiber节点并依次执行effectTag对应操作。如果再遍历一次Fiber树是及其低效的操作,那么怎么办呢?
在completeWork的上层函数completeUnitOfWork中,每个执行完completeWork且存在effectTag的Fiber节点,会被保存在一条被称为effectList的单向链表中。
effectList中第一个Fiber节点白存在fiber.firstEffect,最后一个元素保存在fiber.lastEffect中,在归的阶段,所有有effectTag的Fiber节点都会被追加到effectList中,最终形成以rootFiber.firstEffect为起点的单向链表。这样,在commit阶段只需要遍历effectList九能执行所有的effect了。
2. commit阶段
在render阶段的工作完成后,调用commitRoot(root)进入到commit阶段。如上一节所述,在rootFiber.firstEffect上保存了一条需要执行副作用的Fiber节点的单向链表,这些Fiber节点的updateQueue中保存了变化的props。这些副作用对应的DOM操作在commit阶段执行。除此之外,一些生命周期的钩子、Hook需要在commit阶段执行。
在commit阶段,我们将两次遍历effectList,根据effectTag调用不同的函数对Fiber进行处理。
2.1 准备工作和收尾工作
- before mutation之前: 这一阶段主要做一些变量赋值、状态重置的工作
- layout之后的工作,主要包括三点内容:
userEffect相关的处理- 性能追踪相关
- 在
commit阶段会触发的一些生命周期的钩子和Hook
2.2 before mutation阶段
在before mutation阶段,需要做的三件事情:
- 处理DOM节点渲染/删除后的autoFocus,blur逻辑
- 调用
getSnapshotBeforeUpdate生命周期钩子 - 调度
useEffect然后,我们来看代码的大致逻辑:
function commitBeforeMutationEffects() {
while (nextEffect !== null) {
const current = nextEffect.alternate;
if (!shouldFireAfterActiveInstanceBlur && focusedInstanceHandle !== null) {
// ...focus blur相关
}
const effectTag = nextEffect.effectTag;
// 调用getSnapshotBeforeUpdate
if ((effectTag & Snapshot) !== NoEffect) {
commitBeforeMutationEffectOnFiber(current, nextEffect);
}
// 调度useEffect
if ((effectTag & Passive) !== NoEffect) {
if (!rootDoesHavePassiveEffects) {
rootDoesHavePassiveEffects = true;
scheduleCallback(NormalSchedulerPriority, () => {
flushPassiveEffects();
return null;
});
}
}
nextEffect = nextEffect.nextEffect;
}
}
2.3 mutation阶段
在mutation阶段,遍历effectList,执行的函数是commitMutationEffects.这个函数会对每个Fiber节点执行如下三个操作:
- 根据
ContentReset effectTag重置文字节点 - 更新
ref - 根据
effectTag分别处理,其中,effectTag包括Placement|Update|Deletion|Hydrating. 具体的代码实现如下:
function commitMutationEffects(root: FiberRoot, renderPriorityLevel) {
// 遍历effectList
while (nextEffect !== null) {
const effectTag = nextEffect.effectTag;
// 根据 ContentReset effectTag重置文字节点
if (effectTag & ContentReset) {
commitResetTextContent(nextEffect);
}
// 更新ref
if (effectTag & Ref) {
const current = nextEffect.alternate;
if (current !== null) {
commitDetachRef(current);
}
}
// 根据 effectTag 分别处理
const primaryEffectTag =
effectTag & (Placement | Update | Deletion | Hydrating);
switch (primaryEffectTag) {
// 插入DOM
case Placement: {
commitPlacement(nextEffect);
nextEffect.effectTag &= ~Placement;
break;
}
// 插入DOM 并 更新DOM
case PlacementAndUpdate: {
// 插入
commitPlacement(nextEffect);
nextEffect.effectTag &= ~Placement;
// 更新
const current = nextEffect.alternate;
commitWork(current, nextEffect);
break;
}
// SSR
case Hydrating: {
nextEffect.effectTag &= ~Hydrating;
break;
}
// SSR
case HydratingAndUpdate: {
nextEffect.effectTag &= ~Hydrating;
const current = nextEffect.alternate;
commitWork(current, nextEffect);
break;
}
// 更新DOM
case Update: {
const current = nextEffect.alternate;
commitWork(current, nextEffect);
break;
}
// 删除DOM
case Deletion: {
commitDeletion(root, nextEffect, renderPriorityLevel);
break;
}
}
nextEffect = nextEffect.nextEffect;
}
}
在这里,我们只关注Placement、Update和Deletion这三个case中的内容。
2.3.1 Placement
从前面的代码中可以看出,Placement调用的方法是commitPlacement。它所做的工作分为三步:
- 获取父级DOM节点。
- 获取Fiber节点的DOM兄弟节点
- 根据兄弟节点是否存在决定调用
parentNode.insertBefore或parentNode.appendChild执行DOM插入操作
2.3.2 Update
当Fiber节点含有Update effectTag,意味着该Fiber节点需要更新。调用的方法为commitWork,他会根据Fiber.tag分别处理。
2.3.3 Deletion
当Fiber节点含有Deletion effectTag,意味着该Fiber节点对应的DOM节点需要从页面中删除。调用的方法为commitDeletion。该方法会执行如下操作:
- 递归调用Fiber节点及其子孙Fiber节点中
fiber.tag为ClassComponent的componentWillUnmont生命周期钩子,从页面中移除Fiber节点对应DOM节点 - 解绑ref
- 调用
useEffect的销毁函数
2.4 layout阶段
在Layout阶段,遍历effectTag,执行的函数是commitLayoutEffects。这个函数一共做了两件事:
- commitLayoutEffectOnFiber:调用生命周期钩子和Hook相关操作
- commitAttachRef:赋值ref 具体实现的代码如下:
function commitLayoutEffects(root: FiberRoot, committedLanes: Lanes) {
while (nextEffect !== null) {
const effectTag = nextEffect.effectTag;
// 调用生命周期钩子和hook
if (effectTag & (Update | Callback)) {
const current = nextEffect.alternate;
commitLayoutEffectOnFiber(root, current, nextEffect, committedLanes);
}
// 赋值ref
if (effectTag & Ref) {
commitAttachRef(nextEffect);
}
nextEffect = nextEffect.nextEffect;
}
}