该阶段之所以称为
layout
,因为该阶段的代码都是在
DOM
修改完成(
mutation阶段
完成)后执行的。
注意:由于 JS 的同步执行阻塞了主线程,所以此时 JS 已经可以获取到新的
DOM
,但是浏览器对新的
DOM
并没有完成渲染。
该阶段触发的生命周期钩子和
hook
可以直接访问到已经改变后的
DOM
,即该阶段是可以参与
DOM layout
的阶段。
# 概览
与前两个阶段类似,
layout阶段
也是遍历
effectList
,执行函数。
具体执行的函数是
commitLayoutEffects
。
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
代码如下:
你可以在 这里 看到
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
方法会根据
fiber.tag
对不同类型的节点分别处理。
你可以在 这里 看到
commitLayoutEffectOnFiber源码(commitLayoutEffectOnFiber为别名,方法原名为commitLifeCycles)
-
对于
ClassComponent,他会通过current === null?区分是mount还是update,调用componentDidMount或componentDidUpdate。
触发
状态更新
的
this.setState
如果赋值了第二个参数
回调函数
,也会在此时调用。
this.setState({ xxx: 1 }, () => {
console.log("i am update~");
});
-
对于
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;
你可以从 这里 看到这段代码
在上一节介绍
Update effect
时介绍过,
mutation阶段
会执行
useLayoutEffect hook
的
销毁函数
。
结合这里我们可以发现,
useLayoutEffect hook
从上一次更新的
销毁函数
调用到本次更新的
回调函数
调用是同步执行的。
而
useEffect
则需要先调度,在
Layout阶段
完成后再异步执行。
这就是
useLayoutEffect
与
useEffect
的区别。
-
对于
HostRoot,即rootFiber,如果赋值了第三个参数回调函数,也会在此时调用。
ReactDOM.render(<App />, document.querySelector("#root"), function() {
console.log("i am mount~");
});
# commitAttachRef
commitLayoutEffects
会做的第二件事是
commitAttachRef
。
你可以在 这里 看到
commitAttachRef源码
function commitAttachRef(finishedWork: Fiber) {
const ref = finishedWork.ref;
if (ref !== null) {
const instance = finishedWork.stateNode;
// 获取DOM实例
let instanceToUse;
switch (finishedWork.tag) {
case HostComponent:
instanceToUse = getPublicInstance(instance);
break;
default:
instanceToUse = instance;
if (typeof ref === "function") {
// 如果ref是函数形式,调用回调函数
ref(instanceToUse);
} else {
// 如果ref是ref实例形式,赋值ref.current
ref.current = instanceToUse;
代码逻辑很简单:获取
DOM
实例,更新
ref
。
# current Fiber树切换
至此,整个
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
就是更新后的。
# 总结
从这节我们学到,
layout阶段
会遍历
effectList
,依次执行
commitLayoutEffects
。该方法的主要工作为“根据
effectTag
调用不同的处理函数处理
Fiber
并更新
ref
。
# 参考资料
mutation阶段