书籍传送门:React技术揭秘——卡颂
前言:整篇文章,我都将以
问题
+笔记
的形式作为精读的导语展开叙述~文末有彩蛋喔~~~
上一篇文章中,我们已经了解了架构篇的Commit
阶段中的mutation
和 before mutation
阶段,这一节我们继续上一节,聊一下Commit
阶段的 layout
。
在聊之前,我们先来解答一下,上一节留下的问题:
- 一个组件中多个
useEffect
的执行顺序是固定的吗?
答:是固定的,一个组件内部的 useEffect
会形成一个环状的链表结构,所以会顺序执行。
- 一个组件中如果出现多个
useEffect
和useLayoutEffect
,执行顺序是怎样的?
答:先执行 useLayoutEffect
,后执行 useEffect
。
- 嵌套组件中的
useEffect
和useLayoutEffect
的执行顺序又是怎样的?
答:嵌套组件中的 useEffect
和 useLayoutEffect
,会按照最深的子组件,依次往上执行,按照 render
阶段形成的 effectList
。
补充
const Test = (props) => {
const [s, setS] = useState(1)
console.log(`render ${props.name}`)
useEffect(() => {
const name = props.name
console.log(`effect ${props.name}`)
return () => console.log(`effect cleanup ${name}`)
})
useLayoutEffect(() => {
const name = props.name
console.log(`layout effect ${props.name}`)
return () => console.log(`layout cleanup ${name}`)
})
return (
<>
<button onClick={() => setS(s+1)}>update {s}</button>
<Child name="a" />
<Child name="b" />
</>
)
}
const Child = (props) => {
console.log(`render ${props.name}`)
useEffect(() => {
const name = props.name
console.log(`effect ${props.name}`)
return () => console.log(`effect cleanup ${name}`)
})
useLayoutEffect(() => {
const name = props.name
console.log(`layout effect ${props.name}`)
return () => console.log(`layout cleanup ${name}`)
})
return <></>
}
# 观测执行顺序
render parent
render a
render b
layout cleanup a
layout cleanup b
layout cleanup parent
layout effect a
layout effect b
layout effect parent
effect cleanup a
effect a
effect cleanup b
effect b
effect cleanup parent
effect parent
layout
阶段,同样从以下时机阐述:
layout
阶段也是遍历 effectList
,并且执行 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;
}
}
该函数大致做了两件事:
commitLayoutEffectOnFiber
( 调用生命周期钩子和hook
相关操作)commitAttachRef
(赋值ref
)
commitLayoutEffectOnFiber
时,会针对两种类型的组件做不同的处理:
classComponent
- 会通过
current === null
?区分是mount
还是update
,调用componentDidMount
或componentDidUpdate
。 - 如果在
this.setState
中设置了callback
,也会在此时执行。
- 会通过
FunctionComponent
- 会调用
useLayoutEffect hook
的回调函数,调度useEffect
的销毁与回调函数 - 对于
HostRoot
,即rootFiber
,如果赋值了第三个参数回调函数,也会在此时调用。
- 会调用
commitAttachRef
做的事则很简单,获取 DOM
实例,更新 Ref
那么,思考一个问题,componentDidMount
或者 useLayoutEffect
中的 ref
存在DOM
吗?
From StackOverflow
: componentdidmount-called-before-ref-callback
React 官网
传送门 :
React 将在组件挂载时将 DOM 元素传入ref 回调函数并调用,当卸载时传入 null 并调用它。在componentDidMout 和 componentDidUpdate 触发之前,Refs 保证是最新的。
同步源码(mutation
阶段):
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;
}
}
可以看到,在 mutation
阶段就完成了 Ref
更新。
current Fiber
树切换
root.current = finishedWork;
在双缓存机制一节我们介绍过,
workInProgress Fiber
树在commit
阶段完成渲染后会变为current Fiber
树。这行代码的作用就是切换fiberRootNode
指向的current Fiber
树。
那么这行代码为什么在这里呢?(在 mutation
阶段结束后,layout
阶段开始前。)
如何回答这个问题呢?其实我觉得看一下 React
源码位置的这几段注释就可以了,WIP
树要 layout
就必须在 layout
之前完成转换。componentWillUnmount
代表着一次组件的生命周期结束。
// The work-in-progress tree is now the current tree. This must come after
// the mutation phase, so that the previous tree is still current during
// componentWillUnmount, but before the layout phase, so that the finished
// work is current during componentDidMount/Update.
答: componentWillUnmount
会在 mutation
阶段执行。此时 current Fiber
树还指向前一次更新的 Fiber
树,在生命周期钩子内获取的 DOM
还是更新前的。