-
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的处理, 比如合成事件, 也可能会更贴近实际开发去看待源码等等等