commitRoot
方法是commit
阶段工作的起点。fiberRootNode
会作为传参。
commitRoot(root);
在rootFiber.firstEffect
上保存了一条需要执行副作用的Fiber节点
的单向链表effectList
,这些Fiber节点
的updateQueue
中保存了变化的props
。
这些副作用对应的DOM
操作在commit
阶段执行。
除此之外,一些生命周期钩子(比如componentDidXXX
)、hook
(比如useEffect
)需要在commit
阶段执行。commit
阶段的主要工作(即Renderer
的工作流程)分为三部分:
before mutation
阶段(执行DOM操作前
)mutation
阶段(执行DOM操作
)layout
阶段(执行DOM操作后
)可以从这里看到 commit 阶段的完整代码。
在before mutation
阶段之前和layout
阶段之后还有一些额外工作,涉及到比如useEffect
的触发、优先级相关的重置、ref
的绑定/解绑。
流程概述
before mutation 之前
commitRootImpl
方法中直到第一句if (firstEffect !== null)
之前属于before mutation
之前。
do {
// 触发useEffect回调与其他同步任务。由于这些任务可能触发新的渲染,
// 所以这里要一直遍历执行直到没有任务
flushPassiveEffects();
} while (rootWithPendingPassiveEffects !== null);
// root指 fiberRootNode
// root.finishedWork指当前应用的rootFiber
const finishedWork = root.finishedWork;
// 凡是变量名带lane的都是优先级相关
const lanes = root.finishedLanes;
if (finishedWork === null) {
return null;
}
root.finishedWork = null;
root.finishedLanes = NoLanes;
// 重置Scheduler绑定的回调函数
root.callbackNode = null;
root.callbackId = NoLanes;
let remainingLanes = mergeLanes(finishedWork.lanes, finishedWork.childLanes);
// 重置优先级相关变量
markRootFinished(root, remainingLanes);
// 清除已完成的discrete updates,例如:用户鼠标点击触发的更新。
if (rootsWithPendingDiscreteUpdates !== null) {
if (
!hasDiscreteLanes(remainingLanes) &&
rootsWithPendingDiscreteUpdates.has(root)
) {
rootsWithPendingDiscreteUpdates.delete(root);
}
}
// 重置全局变量
if (root === workInProgressRoot) {
workInProgressRoot = null;
workInProgress = null;
workInProgressRootRenderLanes = NoLanes;
} else {
}
// 将effectList赋值给firstEffect
// 由于每个fiber的effectList只包含他的子孙节点
// 所以根节点如果有effectTag则不会被包含进来
// 所以这里将有effectTag的根节点插入到effectList尾部
// 这样才能保证有effect的fiber都在effectList中
let firstEffect;
if (finishedWork.effectTag > PerformedWork) {
if (finishedWork.lastEffect !== null) {
finishedWork.lastEffect.nextEffect = finishedWork;
firstEffect = finishedWork.firstEffect;
} else {
firstEffect = finishedWork;
}
} else {
// 根节点没有effectTag
firstEffect = finishedWork.firstEffect;
}
可以看到,before mutation
之前主要做一些变量赋值,状态重置的工作。
这一长串代码我们只需要关注最后赋值的firstEffect
,在commit
的三个子阶段都会用到他。
layout 之后
const rootDidHavePassiveEffects = rootDoesHavePassiveEffects;
// useEffect相关
if (rootDoesHavePassiveEffects) {
rootDoesHavePassiveEffects = false;
rootWithPendingPassiveEffects = root;
pendingPassiveEffectsLanes = lanes;
pendingPassiveEffectsRenderPriority = renderPriorityLevel;
} else {
}
// 性能优化相关
if (remainingLanes !== NoLanes) {
if (enableSchedulerTracing) {
// ...
}
} else {
// ...
}
// 性能优化相关
if (enableSchedulerTracing) {
if (!rootDidHavePassiveEffects) {
// ...
}
}
// ...检测无限循环的同步任务
if (remainingLanes === SyncLane) {
// ...
}
// 在离开commitRoot函数前调用,触发一次新的调度,确保任何附加的任务被调度
ensureRootIsScheduled(root, now());
// ...处理未捕获错误及老版本遗留的边界问题
// 执行同步任务,这样同步任务不需要等到下次事件循环再执行
// 比如在 componentDidMount 中执行 setState 创建的更新会在这里被同步执行
// 或useLayoutEffect
flushSyncCallbackQueue();
return null;
你可以在这里看到这段代码
主要包括三点内容:
useEffect 相关的处理。
性能追踪相关。
源码里有很多和 interaction 相关的变量。他们都和追踪 React 渲染时间、性能相关,Profiler API和DevTools中使用。可以在这里看到interaction 的定义
在 commit 阶段会触发一些生命周期钩子(如 componentDidXXX)和 hook(如 useLayoutEffect、useEffect)。
在这些回调方法中可能触发新的更新,新的更新会开启新的 render-commit 流程。
before mutation 阶段
Renderer
工作的阶段被称为commit
阶段。commit
阶段可以分为三个子阶段:
- before mutation 阶段(执行 DOM 操作前)
- mutation 阶段(执行 DOM 操作)
- layout 阶段(执行 DOM 操作后)
before mutation
阶段的代码很短,整个过程就是遍历effectList
并调用commitBeforeMutationEffects
函数处理。
这部分源码在这里,为了增加可读性,示例代码中删除了不相关的逻辑。
// 保存之前的优先级,以同步优先级执行,执行完毕后恢复之前优先级
const previousLanePriority = getCurrentUpdateLanePriority();
setCurrentUpdateLanePriority(SyncLanePriority);
// 将当前上下文标记为CommitContext,作为commit阶段的标志
const prevExecutionContext = executionContext;
executionContext |= CommitContext;
// 处理focus状态
focusedInstanceHandle = prepareForCommit(root.containerInfo);
shouldFireAfterActiveInstanceBlur = false;
// beforeMutation阶段的主函数
commitBeforeMutationEffects(finishedWork);
focusedInstanceHandle = null;
commitBeforeMutationEffects
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;
}
}
整体可以分为三部分:
- 处理
DOM
节点渲染/删除后的autoFocus
、blur
逻辑。 - 调用
getSnapshotBeforeUpdate
生命周期钩子。 - 调度
useEffect
。
调用getSnapshotBeforeUpdate
commitBeforeMutationEffectOnFiber
是commitBeforeMutationLifeCycles
的别名。
在该方法内会调用getSnapshotBeforeUpdate
。
你可以在这里看到这段逻辑
从React v16
开始,componentWillXXX
钩子前增加了UNSAFE_
前缀。
究其原因,是因为Stack Reconciler
重构为Fiber Reconciler
后,render
阶段的任务可能中断/重新开始,对应的组件在render
阶段的生命周期钩子(即componentWillXXX
)可能触发多次。
这种行为和React v15
不一致,所以标记为UNSAFE_
。
更详细的解释参照这里
为此,React
提供了替代的生命周期钩子getSnapshotBeforeUpdate
。
我们可以看见,getSnapshotBeforeUpdate
是在commit
阶段内的before mutation
阶段调用的,由于commit
阶段是同步的,所以不会遇到多次调用的问题。
调度useEffect
在这几行代码内,scheduleCallback
方法由Scheduler
模块提供,用于以某个优先级异步调度一个回调函数。
// 调度useEffect
if ((effectTag & Passive) !== NoEffect) {
if (!rootDoesHavePassiveEffects) {
rootDoesHavePassiveEffects = true;
scheduleCallback(NormalSchedulerPriority, () => {
// 触发useEffect
flushPassiveEffects();
return null;
});
}
}
在此处,被异步调度的回调函数就是触发useEffect
的方法flushPassiveEffects
。
我们接下来讨论useEffect
如何被异步调度,以及为什么要异步(而不是同步)调度。
如何异步调度
在flushPassiveEffects
方法内部会从全局变量rootWithPendingPassiveEffects
获取effectList
。
关于flushPassiveEffects
的具体讲解参照 useEffect 与 useLayoutEffect 一节effectList
中保存了需要执行副作用的Fiber
节点。其中副作用包括:
- 插入
DOM
节点(Placement
) - 更新
DOM
节点(Update
) - 删除
DOM
节点(Deletion
)
除此外,当一个FunctionComponent
含有useEffect
或useLayoutEffect
,他对应的Fiber
节点也会被赋值effectTag
。
你可以从这里看到 hook 相关的
effectTag
在flushPassiveEffects
方法内部会遍历rootWithPendingPassiveEffects
(即effectList
)执行effect
回调函数。
如果在此时直接执行,rootWithPendingPassiveEffects === null
。layout
之后的代码片段中会根据rootDoesHavePassiveEffects === true?
决定是否赋值rootWithPendingPassiveEffects
。
const rootDidHavePassiveEffects = rootDoesHavePassiveEffects;
if (rootDoesHavePassiveEffects) {
rootDoesHavePassiveEffects = false;
rootWithPendingPassiveEffects = root;
pendingPassiveEffectsLanes = lanes;
pendingPassiveEffectsRenderPriority = renderPriorityLevel;
}
所以整个useEffect
异步调用分为三步:
before mutation
阶段在scheduleCallback
中调度flushPassiveEffects
layout
阶段之后将effectList
赋值给rootWithPendingPassiveEffects
scheduleCallback
触发flushPassiveEffects
,flushPassiveEffects
内部遍历rootWithPendingPassiveEffects
为什么需要异步调用
摘录自React
文档effect 的执行时机:
与
componentDidMount
、componentDidUpdate
不同的是,在浏览器完成布局与绘制之后,传给 useEffect 的函数会延迟调用。这使得它适用于许多常见的副作用场景,比如设置订阅和事件处理等情况,因此不应在函数中执行阻塞浏览器更新屏幕的操作。
可见,useEffect
异步执行的原因主要是防止同步执行时阻塞浏览器渲染。
mutation 阶段
类似before mutation
阶段,mutation
阶段也是遍历effectList
,执行函数。这里执行的是commitMutationEffects
。
nextEffect = firstEffect;
do {
try {
commitMutationEffects(root, renderPriorityLevel);
} catch (error) {
invariant(nextEffect !== null, "Should be working on an effect.");
captureCommitPhaseError(nextEffect, error);
nextEffect = nextEffect.nextEffect;
}
} while (nextEffect !== null);
commitMutationEffects
function commitMutationEffects(
root: FiberRoot,
renderPriorityLevel: ReactPriorityLevel
) {
// 遍历effectList
while (nextEffect !== null) {
setCurrentDebugFiberInDEV(nextEffect);
const flags = nextEffect.flags;
// 是否需要重制文本节点
if (flags & ContentReset) {
commitResetTextContent(nextEffect);
}
// 是否有ref的更新
if (flags & Ref) {
const current = nextEffect.alternate;
if (current !== null) {
commitDetachRef(current);
}
if (enableScopeAPI) {
// TODO: This is a temporary solution that allowed us to transition away
// from React Flare on www.
if (nextEffect.tag === ScopeComponent) {
commitAttachRef(nextEffect);
}
}
}
// Placement:插入dom
// Update:更新属性
// Deletion:删除dom
// Hydrating:SSR相关
const primaryFlags = flags & (Placement | Update | Deletion | Hydrating);
switch (primaryFlags) {
case Placement: {
commitPlacement(nextEffect);
nextEffect.flags &= ~Placement;
break;
}
case PlacementAndUpdate: {
// Placement
commitPlacement(nextEffect);
nextEffect.flags &= ~Placement;
// Update
const current = nextEffect.alternate;
commitWork(current, nextEffect);
break;
}
case Hydrating: {
nextEffect.flags &= ~Hydrating;
break;
}
case HydratingAndUpdate: {
nextEffect.flags &= ~Hydrating;
// Update
const current = nextEffect.alternate;
commitWork(current, nextEffect);
break;
}
case Update: {
const current = nextEffect.alternate;
commitWork(current, nextEffect);
break;
}
case Deletion: {
commitDeletion(root, nextEffect, renderPriorityLevel);
break;
}
}
resetCurrentDebugFiberInDEV();
nextEffect = nextEffect.nextEffect;
}
}
commitMutationEffects
会遍历effectList
,对每个Fiber
节点执行如下三个操作:
- 根据
ContentReset effectTag
重置文字节点 - 更新
ref
- 根据
effectTag
分别处理,其中effectTag
包括(Placement
|Update
|Deletion
|Hydrating
)
Placement effect
当Fiber
节点含有Placement effectTag
,意味着该Fiber
节点对应的DOM
节点需要插入到页面中。
调用的方法为commitPlacement
。
function commitPlacement(finishedWork: Fiber): void {
// 是否支持Mutation,dom环境是支持的
if (!supportsMutation) {
return;
}
const parentFiber = getHostParentFiber(finishedWork);
let parent;
let isContainer;
const parentStateNode = parentFiber.stateNode;
switch (parentFiber.tag) {
case HostComponent:
parent = parentStateNode;
isContainer = false;
break;
case HostRoot:
parent = parentStateNode.containerInfo;
isContainer = true;
break;
case HostPortal:
parent = parentStateNode.containerInfo;
isContainer = true;
break;
case FundamentalComponent:
if (enableFundamentalAPI) {
parent = parentStateNode.instance;
isContainer = false;
}
// eslint-disable-next-line-no-fallthrough
default:
invariant(
false,
"Invalid host parent fiber. This error is likely caused by a bug " +
"in React. Please file an issue."
);
}
if (parentFiber.flags & ContentReset) {
resetTextContent(parent);
parentFiber.flags &= ~ContentReset;
}
// 找到Host类型的兄弟节点
// 插入有两种方式:1。找到兄弟节点,执行insertBefore插入节点
// 2.找到父节点,执行AppendChild插入节点
const before = getHostSibling(finishedWork);
// We only have the top Fiber that was inserted but we need to recurse down its
// children to find all the terminal nodes.
if (isContainer) {
insertOrAppendPlacementNodeIntoContainer(finishedWork, before, parent);
} else {
insertOrAppendPlacementNode(finishedWork, before, parent);
}
}
该方法所做的工作分为三步:
- 获取父级
DOM
节点。其中finishedWork
为传入的Fiber
节点。
const parentFiber = getHostParentFiber(finishedWork);
// 父级DOM节点
const parentStateNode = parentFiber.stateNode;
- 获取
Fiber
节点的DOM
兄弟节点
const before = getHostSibling(finishedWork);
- 根据
DOM
兄弟节点是否存在决定调用parentNode.insertBefore
或parentNode.appendChild
执行DOM
插入操作。
// parentStateNode是否是rootFiber
if (isContainer) {
insertOrAppendPlacementNodeIntoContainer(finishedWork, before, parent);
} else {
insertOrAppendPlacementNode(finishedWork, before, parent);
}
值得注意的是,getHostSibling
(获取兄弟DOM
节点)的执行很耗时,当在同一个父Fiber
节点下依次执行多个插入操作,getHostSibling
算法的复杂度为指数级。
这是由于Fiber
节点不只包括HostComponent
,所以Fiber
树和渲染的DOM
树节点并不是一一对应的。要从Fiber
节点找到DOM
节点很可能跨层级遍历。
考虑如下例子:
function Item() {
return <li><li>;
}
function App() {
return (
<div>
<Item/>
</div>
)
}
ReactDOM.render(<App/>, document.getElementById('root'));
对应的Fiber
树和DOM
树结构为:
// Fiber树
child child child child
rootFiber -----> App -----> div -----> Item -----> li
// DOM树
#root ---> div ---> li
当在div
的子节点Item
前插入一个新节点p
,即App
变为:
function App() {
return (
<div>
<p></p>
<Item />
</div>
);
}
对应的Fiber
树和DOM
树结构为:
// Fiber树
child child child
rootFiber -----> App -----> div -----> p
| sibling child
| -------> Item -----> li
// DOM树
#root ---> div ---> p
|
---> li
此时DOM
节点 p
的兄弟节点为li
,而Fiber
节点 p
对应的兄弟DOM
节点为:
fiberP.sibling.child
即fiber p
的兄弟fiber Item
的子fiber li
Update effect
当Fiber
节点含有Update effectTag
,意味着该Fiber
节点需要更新。调用的方法为commitWork
,他会根据Fiber.tag
分别处理。
function commitWork(current: Fiber | null, finishedWork: Fiber): void {
switch (finishedWork.tag) {
case FunctionComponent:
case ForwardRef:
case MemoComponent:
case SimpleMemoComponent:
case Block: {
// 这些都是和functionComponent相关的
if (
enableProfilerTimer &&
enableProfilerCommitHooks &&
finishedWork.mode & ProfileMode
) {
try {
startLayoutEffectTimer();
// useLayout的销毁函数
commitHookEffectListUnmount(HookLayout | HookHasEffect, finishedWork);
} finally {
recordLayoutEffectDuration(finishedWork);
}
} else {
commitHookEffectListUnmount(HookLayout | HookHasEffect, finishedWork);
}
return;
}
case ClassComponent: {
return;
}
// dom节点相关
case HostComponent: {
const instance: Instance = finishedWork.stateNode;
if (instance != null) {
// Commit the work prepared earlier.
const newProps = finishedWork.memoizedProps;
const oldProps = current !== null ? current.memoizedProps : newProps;
const type = finishedWork.type;
const updatePayload: null | UpdatePayload = (finishedWork.updateQueue: any);
finishedWork.updateQueue = null;
if (updatePayload !== null) {
// 更新dom的属性
commitUpdate(
instance,
updatePayload,
type,
oldProps,
newProps,
finishedWork,
);
}
}
return;
}
case HostText: {
const textInstance: TextInstance = finishedWork.stateNode;
const newText: string = finishedWork.memoizedProps;
const oldText: string =
current !== null ? current.memoizedProps : newText;
commitTextUpdate(textInstance, oldText, newText);
return;
}
case HostRoot: {
if (supportsHydration) {
const root: FiberRoot = finishedWork.stateNode;
if (root.hydrate) {
// We've just hydrated. No need to hydrate again.
root.hydrate = false;
commitHydratedContainer(root.containerInfo);
}
}
return;
}
// ...
}
}
这里我们主要关注FunctionComponent
和HostComponent
。
FunctionComponent mutation
当fiber.tag
为FunctionComponent
,会调用commitHookEffectListUnmount
。该方法会遍历effectList
,执行所有useLayoutEffect hook
的销毁函数。
function commitHookEffectListUnmount(tag: number, finishedWork: Fiber) {
const updateQueue: FunctionComponentUpdateQueue | null = (finishedWork.updateQueue: any);
const lastEffect = updateQueue !== null ? updateQueue.lastEffect : null;
if (lastEffect !== null) {
const firstEffect = lastEffect.next;
let effect = firstEffect;
do {
if ((effect.tag & tag) === tag) {
// Unmount
const destroy = effect.destroy;
effect.destroy = undefined;
if (destroy !== undefined) {
destroy();
}
}
effect = effect.next;
} while (effect !== firstEffect);
}
}
HostComponent mutation
当fiber.tag
为HostComponent
,会调用commitUpdate
。
export function commitUpdate(
domElement: Instance,
updatePayload: Array<mixed>,
type: string,
oldProps: Props,
newProps: Props,
internalInstanceHandle: Object
): void {
// Update the props handle so that we know which props are the ones with
// with current event handlers.
updateFiberProps(domElement, newProps);
// Apply the diff to the DOM node.
updateProperties(domElement, updatePayload, type, oldProps, newProps);
}
最终会在updateProperties
中的updateDOMProperties
中将render
阶段 completeWork
中为Fiber
节点赋值的updateQueue
对应的内容渲染在页面上。
function updateDOMProperties(
domElement: Element,
updatePayload: Array<any>,
wasCustomComponentTag: boolean,
isCustomComponentTag: boolean
): void {
// TODO: Handle wasCustomComponentTag
for (let i = 0; i < updatePayload.length; i += 2) {
const propKey = updatePayload[i];
const propValue = updatePayload[i + 1];
// 处理 style
if (propKey === STYLE) {
setValueForStyles(domElement, propValue);
// 处理 DANGEROUSLY_SET_INNER_HTML
} else if (propKey === DANGEROUSLY_SET_INNER_HTML) {
setInnerHTML(domElement, propValue);
// 处理 children
} else if (propKey === CHILDREN) {
setTextContent(domElement, propValue);
// 处理剩余 props
} else {
setValueForProperty(domElement, propKey, propValue, isCustomComponentTag);
}
}
}
Deletion effect
当Fiber
节点含有Deletion effectTag
,意味着该Fiber
节点对应的DOM
节点需要从页面中删除。调用的方法为commitDeletion
。
你可以在这里看到 commitDeletion 源码
这里主要关注unmountHostComponents
中的commitNestedUnmounts
中的commitUnmount
方法
function commitUnmount(
finishedRoot: FiberRoot,
current: Fiber,
renderPriorityLevel: ReactPriorityLevel,
): void {
onCommitUnmount(current);
switch (current.tag) {
case FunctionComponent:
case ForwardRef:
case MemoComponent:
case SimpleMemoComponent:
case Block: {
const updateQueue: FunctionComponentUpdateQueue | null = (current.updateQueue: any);
if (updateQueue !== null) {
const lastEffect = updateQueue.lastEffect;
if (lastEffect !== null) {
const firstEffect = lastEffect.next;
let effect = firstEffect;
do {
const {destroy, tag} = effect;
if (destroy !== undefined) {
if ((tag & HookPassive) !== NoHookEffect) {
// 当functionComponent被销毁时,useEffect的销毁函数也会被执行
enqueuePendingPassiveHookEffectUnmount(current, effect);
} else {
if (
enableProfilerTimer &&
enableProfilerCommitHooks &&
current.mode & ProfileMode
) {
startLayoutEffectTimer();
safelyCallDestroy(current, destroy);
recordLayoutEffectDuration(current);
} else {
safelyCallDestroy(current, destroy);
}
}
}
effect = effect.next;
} while (effect !== firstEffect);
}
}
return;
}
case ClassComponent: {
safelyDetachRef(current);
const instance = current.stateNode;
if (typeof instance.componentWillUnmount === 'function') {
// 会执行componentWillUnmount钩子函数
safelyCallComponentWillUnmount(current, instance);
}
return;
}
case HostComponent: {
// 解绑ref属性
safelyDetachRef(current);
return;
}
// ...
}
}
该方法会执行如下操作:
- 递归调用
Fiber
节点及其子孙Fiber
节点中fiber.tag
为ClassComponent
的componentWillUnmount
生命周期钩子,从页面移除Fiber
节点对应DOM
节点 - 解绑
ref
- 调度
useEffect
的销毁函数
layout 阶段
该阶段之所以称为layout
,因为该阶段的代码都是在DOM
渲染完成(mutation
阶段完成)后执行的。
该阶段触发的生命周期钩子和hook
可以直接访问到已经改变后的DOM
,即该阶段是可以参与DOM layout
的阶段。
与前两个阶段类似,layout
阶段会遍历effectList
,依次执行commitLayoutEffects
。该方法的主要工作为“根据effectTag
调用不同的处理函数处理Fiber
并更新ref
。
具体执行的函数是commitLayoutEffects
。
// commit阶段完成后,currentFiber就会指向已经渲染好的fiber
root.current = finishedWork;
nextEffect = firstEffect;
do {
try {
commitLayoutEffects(root, lanes);
} catch (error) {
invariant(nextEffect !== null, "Should be working on an effect.");
captureCommitPhaseError(nextEffect, error);
nextEffect = nextEffect.nextEffect;
}
} while (nextEffect !== null);
nextEffect = null;
commitLayoutEffects
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;
}
}
commitLayoutEffects
一共做了两件事:
commitLayoutEffectOnFiber
(调用生命周期钩子和hook
相关操作)commitAttachRef
(赋值ref
)
commitLayoutEffectOnFiber
commitLayoutEffectOnFiber(commitLifeCycles)
方法会根据fiber.tag
对不同类型的节点分别处理。
function commitLifeCycles(
finishedRoot: FiberRoot,
current: Fiber | null,
finishedWork: Fiber,
committedLanes: Lanes,
): void {
switch (finishedWork.tag) {
case FunctionComponent:
case ForwardRef:
case SimpleMemoComponent:
case Block: {
if (
enableProfilerTimer &&
enableProfilerCommitHooks &&
finishedWork.mode & ProfileMode
) {
try {
startLayoutEffectTimer();
commitHookEffectListMount(HookLayout | HookHasEffect, finishedWork);
} finally {
recordLayoutEffectDuration(finishedWork);
}
} else {
commitHookEffectListMount(HookLayout | HookHasEffect, finishedWork);
}
schedulePassiveEffects(finishedWork);
return;
}
case ClassComponent: {
const instance = finishedWork.stateNode;
if (finishedWork.flags & Update) {
if (current === null) {
if (
enableProfilerTimer &&
enableProfilerCommitHooks &&
finishedWork.mode & ProfileMode
) {
try {
startLayoutEffectTimer();
instance.componentDidMount();
} finally {
recordLayoutEffectDuration(finishedWork);
}
} else {
instance.componentDidMount();
}
} else {
const prevProps =
finishedWork.elementType === finishedWork.type
? current.memoizedProps
: resolveDefaultProps(finishedWork.type, current.memoizedProps);
const prevState = current.memoizedState;
if (
enableProfilerTimer &&
enableProfilerCommitHooks &&
finishedWork.mode & ProfileMode
) {
try {
startLayoutEffectTimer();
instance.componentDidUpdate(
prevProps,
prevState,
instance.__reactInternalSnapshotBeforeUpdate,
);
} finally {
recordLayoutEffectDuration(finishedWork);
}
} else {
instance.componentDidUpdate(
prevProps,
prevState,
instance.__reactInternalSnapshotBeforeUpdate,
);
}
}
}
const updateQueue: UpdateQueue<
*,
> | null = (finishedWork.updateQueue: any);
if (updateQueue !== null) {
commitUpdateQueue(finishedWork, updateQueue, instance);
}
return;
}
case HostRoot: {
// TODO: I think this is now always non-null by the time it reaches the
// commit phase. Consider removing the type check.
const updateQueue: UpdateQueue<
*,
> | null = (finishedWork.updateQueue: any);
if (updateQueue !== null) {
let instance = null;
if (finishedWork.child !== null) {
switch (finishedWork.child.tag) {
case HostComponent:
instance = getPublicInstance(finishedWork.child.stateNode);
break;
case ClassComponent:
instance = finishedWork.child.stateNode;
break;
}
}
commitUpdateQueue(finishedWork, updateQueue, instance);
}
return;
}
case HostComponent: {
const instance: Instance = finishedWork.stateNode;
if (current === null && finishedWork.flags & Update) {
const type = finishedWork.type;
const props = finishedWork.memoizedProps;
commitMount(instance, type, props, finishedWork);
}
return;
}
// ...
}
}
- 对于
FunctionComponent
及相关类型,他会调用useLayoutEffect hook
的回调函数,调度useEffect
的销毁与回调函数相关类型指特殊处理后的
FunctionComponent
,比如ForwardRef
、React.memo
包裹的FunctionComponent
switch (finishedWork.tag) {
// 以下都是FunctionComponent及相关类型
case FunctionComponent:
case ForwardRef:
case SimpleMemoComponent:
case Block: {
// 执行useLayoutEffect的回调函数
commitHookEffectListMount(HookLayout | HookHasEffect, finishedWork);
// 调度useEffect的销毁函数与回调函数
schedulePassiveEffects(finishedWork);
return;
}
由于mutation
阶段会执行useLayoutEffect hook
的销毁函数。
结合这里我们可以发现,useLayoutEffect hook
从上一次更新的销毁函数调用到本次更新的回调函数调用是同步执行的。
而useEffect
则需要先调度,在Layout
阶段完成后再异步执行。
这就是useLayoutEffect
与useEffect
的区别。
- 对于
ClassComponent
,他会通过current === null?
区分是mount
还是update
,调用componentDidMount或componentDidUpdate。
触发状态更新的this.setState
如果赋值了第二个参数回调函数,也会在此时调用。
commitAttachRef
commitLayoutEffects
会做的第二件事是commitAttachRef
,获取DOM
实例,更新ref
。
function commitAttachRef(finishedWork: Fiber) {
const ref = finishedWork.ref;
if (ref !== null) {
// 获取DOM实例
const instance = finishedWork.stateNode;
let instanceToUse;
switch (finishedWork.tag) {
case HostComponent:
instanceToUse = getPublicInstance(instance);
break;
default:
instanceToUse = instance;
}
if (enableScopeAPI && finishedWork.tag === ScopeComponent) {
instanceToUse = instance;
}
if (typeof ref === "function") {
// 如果ref是函数形式,调用回调函数
ref(instanceToUse);
} else {
// 如果ref是ref实例形式,赋值ref.current
ref.current = instanceToUse;
}
}
}
current Fiber 树切换
至此,整个 layout 阶段就结束了。
前面也讲过,在 layout 阶段开始之前,有这么一段代码:
root.current = finishedWork;
由于在双缓存机制,workInProgress Fiber
树在commit
阶段完成渲染后会变为current Fiber
树。这行代码的作用就是切换fiberRootNode
指向的current Fiber
树。
那么这行代码为什么在这里呢?(在mutation
阶段结束后,layout
阶段开始前。)
那是因为componentWillUnmount
会在mutation
阶段执行。此时current Fiber
树还指向前一次更新的Fiber
树,在生命周期钩子内获取的DOM
还是更新前的。
而componentDidMount
和componentDidUpdate
会在 layout 阶段执行。此时current Fiber
树需要指向更新后的Fiber
树,在生命周期钩子内获取的DOM
就是更新后的。