-
React源码入门篇(一)第一篇文章我们主要初步对React源码进行划分, 然后从
React.createRoot()和ReactDom.reader中的render和updateContainer函数入手 -
react源码阅读(二)- Scheduler 文章二我们从
scheduleUpdateOnFiber入手介绍了解了React的Scheduler -
React源码阅读(三)- render 我们从
renderRootSync入手,走入了React的render阶段
- 走完render之后自然就进入了我们的
commit阶段。 我们在文章二也阐述了入口为finishConcurrentRender
一、finishConcurrentRender
这里主要是根据exitStatus的不同情况进行了switch case的判断, 我们直接找到我们需要的Case: RootCompleted。可以看到直接调用了commitRoot
js
function finishConcurrentRender(root, exitStatus, lanes) {
switch (exitStatus) {
case RootInProgress:
case RootFatalErrored: ....
case RootErrored: ....
case RootSuspended: ....
case RootSuspendedWithDelay: .....
case RootCompleted: {
// The work completed. Ready to commit.
commitRoot(
root,
workInProgressRootRecoverableErrors,
workInProgressTransitions,
);
break;
}
default: {
throw new Error('Unknown root exit status.');
}
}
}
- 我们继续看
commitRoot, 可以看到直接调用了commitRootImpl
二、 commitRootImpl
commitRootImpl整体可以分为三个阶段
commit准备阶段commit阶段:commit阶段会把render阶段计算出的变更应用到DOM上。 具体可以分为三个子阶段before mutation: 操作DOM之前的操作。mutation: 主要是操作DOM, 比如更新移动新增layout: 在DOM操作之后操作
commit结束阶段(这篇篇幅太长了, 这个先跳过)
三、commit准备阶段
commit正式开始之前的逻辑我们通通归纳到准备阶段。 可以看到一开始就来了个循环去调用flushPassiveEffects。 然后是一些对状态的设置,重置。 然后又搞了个调度去调用flushPassiveEffects。核心就是: 清理, 重置, 调度。 具体可见注释
js
// 开启do while循环去处理副作用
do {
flushPassiveEffects();
} while (rootWithPendingPassiveEffects !== null);
...
// 拿到render阶段结束后的成果
const finishedWork = root.finishedWork;
const lanes = root.finishedLanes;
// 重置root的状态
root.finishedWork = null;
root.finishedLanes = NoLanes;
// 清空当前任务, 因为commit是不可打断是同步的, 故可以直接清除, 方便后续安排新的调度
root.callbackNode = null;
root.callbackPriority = NoLane;
// 合并hostRootFiber的lanes和其子孙节点的lanes拿到剩余的待处理的优先级
let remainingLanes = mergeLanes(finishedWork.lanes, finishedWork.childLanes);
// 因为存在并发机制,故存在updateLanes, 此时不能标记其为已完成, 故也需要合并进去
const concurrentlyUpdatedLanes = getConcurrentlyUpdatedLanes();
remainingLanes = mergeLanes(remainingLanes, concurrentlyUpdatedLanes);
// 处理eventTimes和expirationTimes等状态
markRootFinished(root, remainingLanes);
// 在render最后的处理中,我们都将workInProgressRoot置为空了
// 这里对其再次进行判断, 如果render阶段没有成功置空的话这里进行置空
if (root === workInProgressRoot) {
workInProgressRoot = null;
workInProgress = null;
workInProgressRootRenderLanes = NoLanes;
}
// Passive标记对应的是useEffect的副作用操作
// 如果存在挂起的副作用, 那么则通过scheduleCallback生成一个task任务去处理
if (
(finishedWork.subtreeFlags & PassiveMask) !== NoFlags ||
(finishedWork.flags & PassiveMask) !== NoFlags
) {
if (!rootDoesHavePassiveEffects) {
rootDoesHavePassiveEffects = true;
pendingPassiveEffectsRemainingLanes = remainingLanes;
pendingPassiveTransitions = transitions;
// 通过scheduleCallback调度了一个宏任务去处理flushPassiveEffects。
// 具体逻辑可以看第二章, 其中就涉及了该函数
scheduleCallback(NormalSchedulerPriority, () => {
flushPassiveEffects();
return null;
});
}
}
flushPassiveEffects
具体的flushPassiveEffects我们后面接触函数组件再去仔细了解
在上面的代码中我们一共两次调用了该函数。 我们分为前后去介绍这两个函数
- 第一个: 因为我们前前后后可能执行了多个
commit。那么我们需要保证本次commit的副作用在下一次commit开启之前全部执行完。 故需要在每一次的commit流程开始之前, 需要先调用一次flushPassiveEffects清空上一次commit可能遗留下来的副作用 - 第二个: 我们是通过
schduleCallback的方法进行调度, 因为commit的逻辑是同步不可打断的。 故通过schduleCallback调度后就可以保证flushPassiveEffects是在DOM渲染完成之后执行的(本质上是利用了MessageChannel, 具体可见文章二)
markRootFinished
可以看到逻辑上就是根据pendinglanes和remainingLanes拿到noLongerPendingLanes. 即为已经完成任务的优先级。 然后通过while循环去重置对应赛道下标的eventTimes,expirationTimes和entanglements
js
export function markRootFinished(root: FiberRoot, remainingLanes: Lanes) {
// 通过是释放的操作, 拿到已经完成的(不再等待的)优先级
const noLongerPendingLanes = root.pendingLanes & ~remainingLanes;
// 赋值给root的pendinglanes
root.pendingLanes = remainingLanes;
....
// 根据noLongerPendingLanes清理已经处理完的任务
let lanes = noLongerPendingLanes;
while (lanes > 0) {
const index = pickArbitraryLaneIndex(lanes);
const lane = 1 << index;
entanglements[index] = NoLanes;
eventTimes[index] = NoTimestamp;
expirationTimes[index] = NoTimestamp;
....
lanes &= ~lane; // 继续找下一个处理
}
}
四、commit阶段
commit阶段的流程不是一定进行的, 会先进行判断flags。一共是判断了四种flags: BeforeMutationMask, MutationMask,LayoutMasK, PassiveMask
- 这里比较了
hostRootFiber的flags以及subtreeFlags。 因为我们之前在completeWork进行了flags的冒泡处理, 故这里可以直接通过finishedWork.subtreeFlags进行判断,而不是遍历每一个子节点。 准备阶段中判断PassiveMask的逻辑同理。 - 如果存在副作用的话就走入真正的
commit阶段, 没有的话就直接切换树
js
// 检查构造好的Fiber的子孙节点是否存在副作用需要操作
const subtreeHasEffects =
(finishedWork.subtreeFlags &
(BeforeMutationMask | MutationMask | LayoutMask | PassiveMask)) !==
NoFlags;
// 检查hostRootFiber本身是否存放副作用需要进行操作
const rootHasEffect =
(finishedWork.flags &
(BeforeMutationMask | MutationMask | LayoutMask | PassiveMask)) !==
NoFlags;
// 只要存在副作用, 那么则进入commit阶段
if (subtreeHasEffects || rootHasEffect) {
...
} else {
// 没有副作用的话直接切换树了
root.current = finishedWork;
}
真正的commit逻辑如下。 先来一波一览众山小, 总的逻辑是非常清晰的。 先记录了优先级和上下文,然后修改为commit阶段所需要的内容, 等commit的三个阶段完成之后进行重置。其中
- 调用了
commitBeforeMutationEffects进入beforeMutation阶段。 - 调用
commitMutationEffects进入了mutation阶段 - 调用
commitLayoutEffects进入了layout阶段 - 最后调用了
requestPaint让浏览器进行渲染
js
ReactCurrentBatchConfig.transition = null;
// 记录了当前的优先级
const previousPriority = getCurrentUpdatePriority();
// 将当前的优先级设置为同步优先级(最高)
setCurrentUpdatePriority(DiscreteEventPriority);
// 记录当前上下文
const prevExecutionContext = executionContext;
// 更新上下文为CommitContext
executionContext |= CommitContext;
ReactCurrentOwner.current = null;
-------------beforeMutation----------------------------------
const shouldFireAfterActiveInstanceBlur = commitBeforeMutationEffects(
root,
finishedWork,
);
......
--------------mutation阶段------------------------------------
commitMutationEffects(root, finishedWork, lanes);
// 这里重置了containerInfo相关信息
resetAfterCommit(root.containerInfo);
// 切换workInProgress树到current树
// 因为当前的Fiber和DOM已经算更新完了,故对于下一次更新而言,
// 也需要拿到当前当前页面使用的Fiber结构。 故需要进行切换
root.current = finishedWork;
---------------layout阶段-----------------------------------------
commitLayoutEffects(finishedWork, root, lanes);
// 通过浏览器开始渲染
requestPaint();
// 将上下文以及优先级恢复
executionContext = prevExecutionContext;
setCurrentUpdatePriority(previousPriority);
ReactCurrentBatchConfig.transition = prevTransition;
五、before mutation
先看入口函数commitBeforeMutationEffects, 传入的root为根应用节点, firstChild为render阶段构造好的Fiber树。 首先将nextEffect初始化为传入的Fiber树, 接着可以看到他调用了关键函数commitBeforeMutationEffects_begin(看名字就知道他关键)
ts
export function commitBeforeMutationEffects(
root: FiberRoot,
firstChild: Fiber,
) {
focusedInstanceHandle = prepareForCommit(root.containerInfo);
nextEffect = firstChild;
commitBeforeMutationEffects_begin();
const shouldFire = shouldFireAfterActiveInstanceBlur;
shouldFireAfterActiveInstanceBlur = false;
focusedInstanceHandle = null;
return shouldFire;
}
commitBeforeMutationEffects_begin
大概扫一眼你就能发现这函数里面还调用commitBeforeMutationEffects_complete这个东西, 诶这不就跟前面所了解的beginwork和completeWork的命名很像吗. 看来,这个也是去找寻Fiber树的过程
我们一起探究下这像不像那回事。 该函数主要通过while循环, 基于substressFlags找到节点符合BeforeMutationMask的。 总体通过Child链路从头找到尾
ts
function commitBeforeMutationEffects_begin() {
while (nextEffect !== null) {
const fiber = nextEffect; // 当前的Fiber
const child = fiber.child; // 记录下的他的Child
// 如果他存在子节点且他的子节点存在flags标记符合BeforeMutationMask
// 那么就跳过继续向下找
if (
(fiber.subtreeFlags & BeforeMutationMask) !== NoFlags &&
child !== null
) {
child.return = fiber;
nextEffect = child; // 那么就继续找他的子节点
} else {
// 不符合上述条件时即调用
commitBeforeMutationEffects_complete();
}
}
}
commitBeforeMutationEffects_complete
这个函数进来也是一个while寻找, 判断条件依旧是nextEffect
js
function commitBeforeMutationEffects_complete() {
while (nextEffect !== null) {
const fiber = nextEffect;
try {
// 处理当前节点
commitBeforeMutationEffectsOnFiber(fiber);
} catch (error) {
// 报错的情况下才会走入
captureCommitPhaseError(fiber, fiber.return, error);
}
// 寻找他的兄弟节点
const sibling = fiber.sibling;
if (sibling !== null) { // 如果存在的话
sibling.return = fiber.return;
nextEffect = sibling; // 继续判断他的兄弟节点
return; // 此时直接return出去, 又会走到commitBeforeMutationEffects_begin的逻辑
}
// 如果不存在兄弟节点的话那么往回走
nextEffect = fiber.return;
}
}
commitBeforeMutationEffectsOnFiber
可以看到该函数主要就是处理了flags存在Snapshot的节点。 只要满足(flags & Snapshot) !== NoFlags才会走入真正的操作逻辑。 其中的逻辑又是通过tag去对不同节点做不同的处理。 当然Snapshot主要还是考虑类组件和hostRootFiber
主要有两个地方会打上Snapshot标签
- 第一是因为
class组件存在getSnapshotBeforeUpdate方法。 具体可看beginWork->updateClassComponet->updateClassInstance
- 第二是
completeWork中Case HostRoot的时候。 其注释也给出了原因--> 为了在commit的初始阶段时清空container
对于类组件我们可以回顾一下的
getSnapshotBeforeUpdate的用法并思考如何处理
getSnapshotBeforeUpdate()方法在提交到 DOM 节之前调用---------刚好就是beforeMutation- 在
getSnapshotBeforeUpdate()方法中,我们可以访问更新前的props和state---------需要拿到之前的state和props作为参数传入- 其返回值将作为参数传递给
componetDidUpdate()-----需要拿到该函数的返回值并记录
具体看看commitBeforeMutationEffectsOnFiber是如何处理的
js
function commitBeforeMutationEffectsOnFiber(finishedWork: Fiber) {
const current = finishedWork.alternate;
const flags = finishedWork.flags; // 当前节点的flags标志
// 只要标志为Snapshot才会进行处理
if ((flags & Snapshot) !== NoFlags) {
switch (finishedWork.tag) {
case FunctionComponent:
case ForwardRef:
case SimpleMemoComponent: {
break;
}
// 对于类组件
case ClassComponent: {
if (current !== null) {
const prevProps = current.memoizedProps; // 拿到之前的props
const prevState = current.memoizedState; // 拿到之前的state
const instance = finishedWork.stateNode; // 拿到组件实例
// 在这里调用了getSnapshotBeforeUpdate, 返回值赋值给snapshot
const snapshot = instance.getSnapshotBeforeUpdate(
finishedWork.elementType === finishedWork.type
? prevProps
: resolveDefaultProps(finishedWork.type, prevProps),
prevState,
);
// 然后赋值给instance.__reactInternalSnapshotBeforeUpdate进行保存
instance.__reactInternalSnapshotBeforeUpdate = snapshot;
}
break;
}
// 对于hostFiberRoot
case HostRoot: {
if (supportsMutation) {
const root = finishedWork.stateNode; // 拿到Root根应用节点
// root.containerInfo执行根DOM节点, 此处调用clearContainer进行清空处理
clearContainer(root.containerInfo);
}
break;
}
.....
}
}
小总结
这一段的逻辑还是相对简单很多的, 因为目标很明显且单一。 就是基于subtreeFlags查找Fiber树,然后找到flags带有Snapshot的节点进行处理。 如果是类组件则调用其getSnapshotBeforeUpdate函数。 如果是hostFiberRoot则对根DOM节点进行清空处理
我顺着逻辑画了个图, 可以注意到几个点
- 如果子节点不存在
BeforeMutationMask是不会继续往下走的,例如图右下的节点,他会直接找他的兄弟节点, 若没有的话则直接回父节点了, 故不是所有的节点都会调用commitBeforeMutationEffectsOnFiber方法。 commitBeforeMutationEffectsOnFiber调用的顺序有点类似我们常说的二叉树的中序遍历, 先找到底,处理完其子节点调用, 再回到它本身调用xxxbeigin和xxxcomplete的调用逻辑和之前的beiginWork和completeWork调用逻辑很像。都是靠beigin往深处挖掘,然后靠complete找兄弟节点和往回走
这个图建议多看看。React很多个地方都采用了这个逻辑。包括后面的layout。 争取举一反三

六、mutation
先看入口函数commitMutationEffects, 这里没有什么逻辑, 就是设置了inProgressLanes和inProgressRoot, 然后调用了commitMutationEffectsOnFiber, 调用完再清空
ts
export function commitMutationEffects(
root: FiberRoot,
finishedWork: Fiber,
committedLanes: Lanes,
) {
// 调用前设置优先级和root
inProgressLanes = committedLanes;
inProgressRoot = root;
commitMutationEffectsOnFiber(finishedWork, root, committedLanes);
// 调用完置空
inProgressLanes = null;
inProgressRoot = null;
}
commitMutationEffectsOnFiber
主体逻辑还是通过Switch Case对不同的节点进行了不同的处理。 这里比较冗长我直接都省略了。但是无论tag是什么, 一定有两个函数会执行,可以看到正是default里面的recursivelyTraverseMutationEffects和commitReconciliationEffects。 我们先了解一下这两个函数, 后面再细分tag去了解
ts
function commitMutationEffectsOnFiber(
finishedWork: Fiber,
root: FiberRoot,
lanes: Lanes,
) {
const current = finishedWork.alternate; // 拿到当前页面使用的Fiber结构
const flags = finishedWork.flags; // 拿到当前节点的标签
switch (finishedWork.tag) {
case FunctionComponent:
case ForwardRef:
case MemoComponent:
case SimpleMemoComponent: ......
case ClassComponent: ....
case HostComponent: .....
case HostText: .....
case HostRoot: ....
case HostPortal: ....
case SuspenseComponent: ....
case OffscreenComponent:....
case SuspenseListComponent: ....
case ScopeComponent: ......
default: {
recursivelyTraverseMutationEffects(root, finishedWork, lanes);
commitReconciliationEffects(finishedWork);
return;
}
}
}
recursivelyTraverseMutationEffects
- 从函数命名-递归遍历
mutation副作用。 就知道这必为核心函数。 故我们要比较细致地去看这个函数 - 他分成两部分
- 优先处理了
deletions(进行其他操作也需要循环Fiber树,此时如果有该被删除的节点但是还没有被删除的,容易有意外的结果. 所以我们在操作一个Fiber节点时,一定是先考虑删除操作),- OK我们先看一下
render阶段对于删除节点我们是如何处理的:- 主要是父节点
flags打上了ChildDeletion(这里用于匹配MutationMask, 让其能够进入commit流程) - 然后父节点的
deletetions属性push进去需要删除的节点(这里用于拿到具体的目标删除节点--这里是旧节点哦)
- 主要是父节点
- 在
commit阶段这里采用了commitDeletionEffects作为删除的入口
- OK我们先看一下
- 然后再通过
while去处理符合MutationMask的节点, 递归的思想就体现在这里, 这里的逻辑是顺着Child链和Sibling链, 然后去遍历到每一个子节点, 然后又调用了我们前面讲述的commitMutationEffectsOnFiber, 形成了递归
- 优先处理了
js
function recursivelyTraverseMutationEffects(
root: FiberRoot,
parentFiber: Fiber,
lanes: Lanes,
) {
// 删除类型可以安排在任何类型的Fiber节点上。需要优先被处理
const deletions = parentFiber.deletions;
if (deletions !== null) { // 如果没有需要删除的节点的话这里为null, 有的话才会是数组
// 遍历需要删除的节点
for (let i = 0; i < deletions.length; i++) {
const childToDelete = deletions[i];
try {
commitDeletionEffects(root, parentFiber, childToDelete);
} catch (error) {
captureCommitPhaseError(childToDelete, parentFiber, error);
}
}
}
// 如果子节点flgas满足MutationMask, 那么又开启了递归遍历进行处理
if (parentFiber.subtreeFlags & MutationMask) {
let child = parentFiber.child;
while (child !== null) {
commitMutationEffectsOnFiber(child, root, lanes);
child = child.sibling;
}
}
}
Deletion
删除的部分会比较详细地讲解, 主打一个举一反三
commitDeletionEffects
该函数为删除的入口函数,这里使用标签语句,从目标删除节点开始, 顺着你return找其父节点。 在Case找到目标tag标签的时候, 记录下stateNode(DOM节点)和设置hostParentIsContainer状态,然后退出Switch且退出while。 接着调用了commitDeletionEffectsOnFiber。
故这个函数的作用很明显, 就是找到hostParent。 因为我们的父Fiber节点可能是ClassComponent, 可能是FunctionComponent,这些节点是无法直接操作DOMAPI的, 故我们会找到最近的能够操作的父节点, 拿到他们的stateNode, 也就是我们之前在render阶段的CompleteWork中生成的DOM节点
js
function commitDeletionEffects(
root: FiberRoot,
returnFiber: Fiber,
deletedFiber: Fiber,
) {
let parent = returnFiber;
findParent: while (parent !== null) {
switch (parent.tag) {
case HostComponent: {
hostParent = parent.stateNode;
hostParentIsContainer = false;
break findParent;
}
case HostRoot: {
hostParent = parent.stateNode.containerInfo;
hostParentIsContainer = true;
break findParent;
}
case HostPortal: {
hostParent = parent.stateNode.containerInfo;
hostParentIsContainer = true;
break findParent;
}
}
parent = parent.return;
}
commitDeletionEffectsOnFiber(root, returnFiber, deletedFiber);
hostParent = null;
hostParentIsContainer = false;
detachFiberMutation(deletedFiber);
}
commitDeletionEffectsOnFiber
这个函数就是实际上操作删除的函数了, 这里依旧是通过区分不同的tag执行不同的方法, 我们简单了解一下几种常见的节点。
js
function commitDeletionEffectsOnFiber(
finishedRoot: FiberRoot,
nearestMountedAncestor: Fiber,
deletedFiber: Fiber,
) {
onCommitUnmount(deletedFiber);
switch (deletedFiber.tag) {
case HostComponent: .....
case HostText: .....
case DehydratedFragment: ...
case HostPortal: ....
case FunctionComponent:
case ForwardRef:
case MemoComponent:
case SimpleMemoComponent: ....
case ClassComponent: ....
case ScopeComponent: ...
case OffscreenComponent: ...
default: {
recursivelyTraverseDeletionEffects(
finishedRoot,
nearestMountedAncestor,
deletedFiber,
);
return;
}
}
}
recursivelyTraverseDeletionEffects
基本上无论走到哪一步都会调用该函数, 我们先了解一下该函数的作用. 可以看到这里这一步核心思想就是。 诶我要被删除了, 那我的子节点们也得一个不留,通通删除。 又形成了递归。 跟recursivelyTraverseMutationEffects中的逻辑没啥两样的
再说准确一点,这里的删除其实不是对DOM的操作。 而是需要通知到每一个节点。 也许他们存在什么删除前的逻辑需要处理(dddd)-- 请看后续讲解
js
function recursivelyTraverseDeletionEffects(
finishedRoot, // 根应用节点
nearestMountedAncestor, // Fiber父节点
parent, // 目标删除节点
) {
let child = parent.child; // 找到第一个子节点
while (child !== null) { // 删除所有子节点
// 通过调用commitDeletionEffectsOnFiber去挨个删除子节点
commitDeletionEffectsOnFiber(finishedRoot, nearestMountedAncestor, child);
child = child.sibling; // 所有子节点
}
}
hostText/hostComponent
这里是Switch Case针对于tag为hostText的措施。 也可以说是分为上下两部分, 上面是递归的逻辑, 下面是通过removeChild实现真正DOM操作删除节点的逻辑
js
const prevHostParent = hostParent; // 先记录其DOM父节点
const prevHostParentIsContainer = hostParentIsContainer;
hostParent = null; // 在递归开始前置空
// 一开始就先执行了递归删除子节点, 跳出了说明子节点为null了
recursivelyTraverseDeletionEffects(
finishedRoot,
nearestMountedAncestor,
deletedFiber,
);
// 递归结束之后设置回来
hostParent = prevHostParent;
hostParentIsContainer = prevHostParentIsContainer;
// 因为递归开始前将其置空了,跳出后有值。 故当hostParent不为空的时候说明此时deletedFiber Child为空
if (hostParent !== null) {
// 这里核心都是通过调用removeChild去操作DOM移出节点
if(hostParentIsContainer) {
removeChildFromContainer(
((hostParent: any): Container),
(deletedFiber.stateNode: Instance | TextInstance),
);
} else {
removeChild(
((hostParent: any): Instance),
(deletedFiber.stateNode: Instance | TextInstance),
);
}
}
return;
来个实例走一遍流程,我们对节点进行卸载, 旧节点树和新节点树如图所示
我们通过打断点, 找到此时的情况, 开始走入删除流程。 找hostParent这一步我们直接跳过, 因为parentFiber本身就是一个hostComponent, 最后hostParent就是指向他的stateNode 
接着走到commitDeletionEffectsOnFiber -> recursivelyTraverseDeletionEffects进入递归。 此时会扫描其子节点。 直接Child为空跳出了递归, 但是除了第一次进入recursivelyTraverseDeletionEffects的上下文之外, 其他上下文拿到的hostParent其实都为空。 故他们也不做删除节点的事。就只有我们目标删除的fiber节点会采用removeChild的逻辑。



那么我们为什么还要去遍历到每一个子节点呢? 我们带着问题走入FunctionComponent和ClassComponent的逻辑。 走入之前我们先改一下我们的Fiber树 

ClassComponent
可以看到这里的逻辑和hostText有明显的不同, 多了一部分也少了一部分
- 少掉的: 少掉的是操作
DOM API的逻辑, 因为Fiber tag为classComponent的时候是不会对应真实DOM结构的, 故这里不需要对应逻辑 - 多出来的, 可以看到主要有两个函数
safelyDetachRef和safelyCallComponentWillUnmount, 我们放在下面详细讲解
js
if (!offscreenSubtreeWasHidden) {
safelyDetachRef(deletedFiber, nearestMountedAncestor);
const instance = deletedFiber.stateNode;
if (typeof instance.componentWillUnmount === 'function') {
safelyCallComponentWillUnmount(
deletedFiber,
nearestMountedAncestor,
instance,
);
}
}
recursivelyTraverseDeletionEffects(
finishedRoot,
nearestMountedAncestor,
deletedFiber,
);
return;
}
- 对于
safelyDetachRef, 直接上图, 从前面代码可以得知, 我们用ref拿了该类组件的实例。 当该类组件要被销毁的时候, 此时应该将ref.current重置为null.(ref还可以是函数, 处理逻辑会更复杂, 这里直接跳过了)。 反正该函数核心目标是处理ref让他拿到正确的值
- 对于
safelyCallComponentWillUnmount, 看名字就知道个七七八八了, 要卸载一个类组件, 如果该类组件设置了ComponentWillUnmount的时候, 那此时就应该拿出来执行了

FunctionComponent
函数组件的逻辑相比其类组件就要复杂一些了。 但是万变不离其宗。 他也是做了一些组件卸载前需要处理的回调。 后面会针对React hook出一篇。 这里就直接简单描述下
我们在该函数组件使用了useEffect, 也使用了useLayoutEffect
这里会去遍历我们的updateQueue, 然后对tag进行识别, 对于useEffect来说tag为9,此时不会走入逻辑中, 对于useLayoutEffect来说tag为5, 此时会调用safelyCallDestroy, 该函数会调用destroy, 如图, 也就是我们useLayoutEffect return出去的函数。 所以我们说useLayoutEffect是同步的。 如果在这里做过多的操作的话那么就会阻塞整个commit的进行

commitReconciliationEffects
讲了一大堆recursivelyTraverseMutationEffects后, 让我们回到recursivelyTraverseMutationEffects的下一个函数commitReconciliationEffects.
看到flags判断Placement就知道这个是用来处理插入节点相关的逻辑。 他可以处理添加和插入。
js
function commitReconciliationEffects(finishedWork: Fiber) {
const flags = finishedWork.flags;
if (flags & Placement) {
try {
commitPlacement(finishedWork);
} catch (error) {
captureCommitPhaseError(finishedWork, finishedWork.return, error);
}
finishedWork.flags &= ~Placement;
}
}
我们再继续看commitPlacement, 这里只有两种类型。 一种就普通的节点。 可以处理插入或者添加节点的操作, 通过insertOrAppendPlacementNode。 这里的逻辑多少和删除有点接近, 就不赘述了
再看对hostRoot, hootPortal的处理.
- 还记得我们曾在
render阶段, 初次渲染的时候通过placeSingleChild给hostRoot打上了Placemenet的标签(文章三讲述过,可以回看)。 这里就是将他拿出来使用的时候
js
case HostRoot:
case HostPortal: {
const parent: Container = parentFiber.stateNode.containerInfo;
const before = getHostSibling(finishedWork);
insertOrAppendPlacementNodeIntoContainer(finishedWork, before, parent);
break;
}
insertOrAppendPlacementNodeIntoContainer。 该函数也是先顺着Child寻找, 找到目标的HostComponent或者HostText。 然后直接将对应的stateNode插入根DOM节点(因为这里传入的parent就是根节点)
js
function insertOrAppendPlacementNodeIntoContainer(
node: Fiber,
before: ?Instance,
parent: Container,
): void {
const {tag} = node;
const isHost = tag === HostComponent || tag === HostText;
if (isHost) {
const stateNode = node.stateNode;
if (before) {
insertInContainerBefore(parent, stateNode, before);
} else {
appendChildToContainer(parent, stateNode);
}
} else if (tag === HostPortal) {
// If the insertion itself is a portal, then we don't want to traverse
// down its children. Instead, we'll get insertions from each child in
// the portal directly.
} else {
const child = node.child;
if (child !== null) {
insertOrAppendPlacementNodeIntoContainer(child, before, parent);
let sibling = child.sibling;
while (sibling !== null) {
insertOrAppendPlacementNodeIntoContainer(sibling, before, parent);
sibling = sibling.sibling;
}
}
}
}
可以看到该函数被调用后, 所有的节点都挂载上去了。为什么挂了一个就能全部挂上去了呢---------> 在render阶段的CompleteWork中, 我们就生成了DOM并让他形成了对应的DOM树。 故就像一个葡萄树, 我们把枝头给挂起来, 那么整串就都挂上了

看完上面的逻辑。再看下面的两个图就很清晰了
- 首次渲染
performance图--commit过程
- 触发了节点的卸载对应的
performance图---commit过程
小总结
回看mutation过程, 主要流程如下
- 操作
DOM, 包括增删改。 在首次渲染的时候还负责将DOM树挂载上去 - 对于类组件, 在组件卸载的时候调用了
ComponentWillUnMount以及重置了ref - 对于函数组件, 在组件卸载的时候调用了
useLayoutEffect的destory函数
七、layout
还是先看入口函数commitLayoutEffects. 可以看到又是先状态初始化, 然后又是xxxxbegin, 调用完再重置状态。 熟悉的套路(扶额笑扶额笑扶额笑)
js
export function commitLayoutEffects(
finishedWork: Fiber,
root: FiberRoot,
committedLanes: Lanes,
): void {
inProgressLanes = committedLanes;
inProgressRoot = root;
nextEffect = finishedWork;
commitLayoutEffects_begin(finishedWork, root, committedLanes);
inProgressLanes = null;
inProgressRoot = null;
}
commitLayoutEffects_begin
这个整体套路和commitBeforeMutationEffects_begin基本一致, 不清楚的可以直接回看之前的函数。 我们这里不赘述了。
js
function commitLayoutEffects_begin(
subtreeRoot: Fiber,
root: FiberRoot,
committedLanes: Lanes,
) {
// Suspense layout effects semantics don't change for legacy roots.
const isModernRoot = (subtreeRoot.mode & ConcurrentMode) !== NoMode;
while (nextEffect !== null) {
const fiber = nextEffect;
const firstChild = fiber.child;
...
if ((fiber.subtreeFlags & LayoutMask) !== NoFlags && firstChild !== null) {
firstChild.return = fiber;
nextEffect = firstChild;
} else {
commitLayoutMountEffects_complete(subtreeRoot, root, committedLanes);
}
}
}
commitLayoutMountEffects_complete
不赘述+1、我们直接看commitLayoutEffectOnFiber想干什么
js
function commitLayoutMountEffects_complete(
subtreeRoot: Fiber,
root: FiberRoot,
committedLanes: Lanes,
) {
while (nextEffect !== null) {
const fiber = nextEffect;
if ((fiber.flags & LayoutMask) !== NoFlags) {
const current = fiber.alternate;
try {
commitLayoutEffectOnFiber(root, current, fiber, committedLanes);
} catch (error) {
captureCommitPhaseError(fiber, fiber.return, error);
}
}
if (fiber === subtreeRoot) {
nextEffect = null;
return;
}
const sibling = fiber.sibling;
if (sibling !== null) {
sibling.return = fiber.return;
nextEffect = sibling;
return;
}
nextEffect = fiber.return;
}
}
commitLayoutEffectOnFiber
这个函数直接拉了坨大的。 整体代码量较多,直接看比较经典的函数组件和类组件即可。 我把核心逻辑都提出来了
js
function commitLayoutEffectOnFiber(
finishedRoot: FiberRoot,
current: Fiber | null,
finishedWork: Fiber,
committedLanes: Lanes,
): void {
if ((finishedWork.flags & LayoutMask) !== NoFlags) {
switch (finishedWork.tag) {
case FunctionComponent:
case ForwardRef:
case SimpleMemoComponent: {
.....
// 对于函数组件来说, 同步去执行useLayoutEffect的回调
commitHookEffectListMount(
HookLayout | HookHasEffect,
finishedWork,
);
.....
break;
}
case ClassComponent: {
const instance = finishedWork.stateNode;
if (finishedWork.flags & Update) {
if (!offscreenSubtreeWasHidden) {
if (current === null) {
...
// 如果是初次挂载的话, 调用componentDidMount
instance.componentDidMount();
} else {
// 如果是更新的话, 那么则调用componentDidUpdate
// 这里传入了instance.__reactInternalSnapshotBeforeUpdate
// 看commitBeforeMutationEffectsOnFiber。 我们在遇到getSnapshotBeforeUpdate的时候处理的
const prevProps =
finishedWork.elementType === finishedWork.type
? current.memoizedProps
: resolveDefaultProps(
finishedWork.type,
current.memoizedProps,
);
const prevState = current.memoizedState;
....
instance.componentDidUpdate(
prevProps,
prevState,
instance.__reactInternalSnapshotBeforeUpdate,
);
}
}
}
// 调用setState的回调
const updateQueue: UpdateQueue<
*,
> | null = (finishedWork.updateQueue: any);
commitUpdateQueue(finishedWork, updateQueue, instance);
}
break;
}
case HostRoot: ...
case HostComponent: ....
case HostText: {
break;
}
case HostPortal: {
break;
}
case Profiler: .....
case SuspenseComponent: ...
case SuspenseListComponent:
case IncompleteClassComponent:
case ScopeComponent:
case OffscreenComponent:
case LegacyHiddenComponent:
case TracingMarkerComponent: {
break;
}
default:
throw new Error(
'This unit of work tag should not have side-effects. This error is ' +
'likely caused by a bug in React. Please file an issue.',
);
}
}
// 进行Ref的绑定逻辑
if (!enableSuspenseLayoutEffectSemantics || !offscreenSubtreeWasHidden) {
if (enableScopeAPI) {
if (finishedWork.flags & Ref && finishedWork.tag !== ScopeComponent) {
commitAttachRef(finishedWork);
}
} else {
if (finishedWork.flags & Ref) {
commitAttachRef(finishedWork);
}
}
}
}
- 可以看到上面的函数最后是进行了
Ref的绑定。 调用的函数是commitAttachRef
js
function commitAttachRef(finishedWork: Fiber) {
const ref = finishedWork.ref;
if (ref !== null) {
// 实际上就是拿到他的stateNode, 也就是他对应的节点。
// 如果是类组件的话就是对应的实例。 函数组件为null
// 普通节点的话就是对应的DOM节点
const instance = finishedWork.stateNode;
let instanceToUse;
switch (finishedWork.tag) {
case HostComponent:
instanceToUse = getPublicInstance(instance);
break;
default:
instanceToUse = instance;
}
// ref是个函数的话则调用。 传入拿到的instanceToUse
if (typeof ref === 'function') {
let retVal;
..
retVal = ref(instanceToUse);
} else {
ref.current = instanceToUse; // 否则的话直接赋值给current即可
}
}
}
小总结
因为before mutation和mutation了解得比较多。 故到layout阶段很多逻辑其实就很清晰了。我们也就是说简单地过了一下
所以它干了啥呢
- 针对函数组件, 调用了
useLayoutEffect的回调 - 针对类组件, 初次挂载的情况下调用
componentDidMount, 更新的情况下调用componentDidUpdate。 以及处理了setState的回调 - 处理了
Ref对象的绑定
这四篇文章整体过了一遍React的基本执行逻辑,整理起来收获还是挺大的,也输出了一些逻辑图。 当然可能会存在一些描述不够恰当, 不够清晰的地方,毕竟整个源码的逻辑是很庞大的。 接下去会更细节地出一些文章比如hook的处理, 比如合成事件, 也可能会更贴近实际开发去看待源码等等等