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;
}
}