TL;DR
useEffect
和 useLayoutEffect
的使用方式相同。例如,使用 useEffect
的代码如下所示:
useEffect(
function create() {
// 创建副作用的回调函数
return function destroy() {
// 清理副作用的回调函数
}
},
// 副作用对应的依赖
[...deps]
)
那么问题来了,React 何时执行上述 create
函数和 destroy
函数呢?
为了回答该问题,本文包括:
- 在虚拟 DOM 树中,两种回调函数的执行顺序。
- useLayoutEffect 的回调何时触发?
- useEffect 的回调何时触发?
在虚拟 DOM 树中的执行顺序
在虚拟 DOM 树结构中,useEffect
和 useLayoutEffect
的回调函数执行顺序是相同的。因此,只要掌握了 useEffect,也就掌握了 useLayoutEffect。例如当虚拟 DOM 结构如下图时:
无论使用的 Hook 是 useEffect
还是 useLayoutEffect
,create
函数的执行顺序都是:
2-1
2-2
1-1
2-3
1-2
先 destroy 再 create
在组件状态更新后,React 将先调用所有 destroy
函数,再调用所有 create
函数。
destroy 顺序
destroy 分为两类:
- 从组件树中删除虚拟 DOM 时,引起的
destroy
函数被调用。 - 组件内 Hook 的依赖发生改变,引起的
destroy
函数被调用。
删除时
React 使用先序遍历,处理删除时的虚拟 DOM,并执行它们的 destroy 函数。
线上 Demo 请戳这里。本节内容对应 Demo 中的 Show1。
const useEffectFunc = React.useEffect
// const useEffectFunc = React.useLayoutEffect;
function Comp({ name, children, v }) {
useEffectFunc(
function create() {
console.log("create effect", name)
return function destroy() {
console.log("destroy effect", name)
}
},
[v]
)
return (
<ul>
<li>
<div>{name}</div>
{children}
</li>
</ul>
)
}
function App() {
const [showComp, setShowComp] = useState(true)
return (
<div>
<div>
<button onClick={() => setShowComp(v => !v)}>
点击{showComp ? "隐藏" : "展示"}
</button>
</div>
{showComp && (
<div>
<Comp name="1-1">
<Comp name="2-1" />
<Comp name="2-2" />
</Comp>
<Comp name="1-2">
<Comp name="2-3" />
</Comp>
</div>
)}
</div>
)
}
点击按钮后,所有 Comp 组件对应的虚拟 DOM 都会被删除,此时 destroy
的调用顺序为:
1-1
2-1
2-2
1-2
2-3
更新时
React 使用后序遍历,处理更新时的虚拟 DOM,并执行它们的 destroy 回调。
线上 Demo 请戳这里。本节内容对应 Demo 中的 Show2。
function App() {
const [showComp, setShowComp] = useState(true)
return (
<div>
<div>
<button onClick={() => setShowComp(v => !v)}>点击更新</button>
</div>
<div>
<Comp name="1-1" v={showComp}>
<Comp name="2-1" v={showComp} />
<Comp name="2-2" v={showComp} />
</Comp>
<Comp name="1-2" v={showComp}>
<Comp name="2-3" v={showComp} />
</Comp>
</div>
</div>
)
}
点击按钮后,传给 Comp 组件的 v
发生改变。因为 Comp 中 Hook 依赖于 v
,所以会执行 destroy
函数。此时 destroy
的调用顺序为:
2-1
2-2
1-1
2-3
1-2
同时存在删除和更新时
当组件的 children 既存在被删除的虚拟 DOM,也存在更新的虚拟 DOM 时,会先处理被删除的虚拟 DOM,再处理更新的虚拟 DOM。
线上 Demo 请戳这里。本节内容对应 Demo 中的 Show3。
function App() {
const [showComp, setShowComp] = useState(true)
return (
<div>
<div>
<button onClick={() => setShowComp(v => !v)}>点击更新</button>
</div>
<div>
<Comp name="1-1" v={showComp}>
<Comp name="2-1" v={showComp} />
<Comp name="2-2" v={showComp} key={showComp ? "1" : "0"} />
</Comp>
<Comp name="1-2" v={showComp}>
<Comp name="2-3" v={showComp} />
</Comp>
</div>
</div>
)
}
点击按钮后,因为 <Comp name="2-2" key={} />
的 key 发生改变,所以它会被删除,然后重新创建。此时 destroy
的调用顺序为:
2-2
2-1
1-1
2-3
1-2
伪代码
destroy
回调在虚拟 DOM 树中的执行顺序伪代码如下:
function execDestroy(node) {
// 执行该虚拟 DOM 相关的 destroy 回调
}
function travelDestroy(node) {
for (const deletedChild of node.deletions) {
// 如果该虚拟 DOM 的 children 存在删除,则处理每个被删除的虚拟 DOM
travelDeletion(deletedChild)
}
for (const child of node) {
travelDestroy(child)
}
execDestroy(node)
}
function travelDeletion(node) {
execDestroy(node)
for (const child of node.children) {
travelDeletion(child)
}
}
// React 最初调用
travelDestroy(root)
create 顺序
React 使用后序遍历虚拟 DOM 树的方式,执行它们的 create
回调。
线上 Demo 请戳这里。Demo 中所有例子的 create 回调执行顺序是相同的。其结果为:
2-1
2-2
1-1
2-3
1-2
伪代码
create
回调在虚拟 DOM 树中的执行顺序伪代码如下:
function execCreate(node) {
// 执行该虚拟 DOM 相关的 create 回调
}
function travelCreate(node) {
for (const child of node) {
travelCreate(child)
}
execCreate(node)
}
// React 最初调用
travelCreate(root)
useEffect vs useLayoutEffect
useLayoutEffect 的回调是在提交阶段同步执行的,而 useEffect 是在提交阶段完成后的未来某时刻执行。因此,在 useEffect 的回调中更新组件状态或修改 DOM,往往会引起页面闪一下的 bug。当遇到这类问题时,应使用 useLayoutEffect 代替 useEffect,因为同步执行的代码一定在浏览器重绘之前执行。
何时触发 useEffect 的回调函数
上面谈到 useEffect 的回调会在未来某时刻执行,那具体是什么时候呢?
React 源码将 useEffect 回调的处理交给了 Scheduler 进行调度。React 源码如下:
if (
(finishedWork.subtreeFlags & PassiveMask) !== NoFlags ||
(finishedWork.flags & PassiveMask) !== NoFlags
) {
if (!rootDoesHavePassiveEffects) {
rootDoesHavePassiveEffects = true
scheduleCallback(NormalSchedulerPriority, () => {
flushPassiveEffects()
return null
})
}
}
因为调度的优先级是 NormalSchedulerPriority
,所以该任务最快也要在下个宏任务中才会执行。根据以上代码,我们可将 useEffect 的回调称为 Passive Effect。
但 useEffect 的回调函数不一定在下个宏任务中执行。如果在 useEffect 的回调触发之前,React 组件又进行了一次状态更新,React 会先将之前的 Passive Effect 都处理掉。
所以 useEffect 的回调不一定在浏览器重绘之后才执行。在以下代码中,useEffect 的回调就是在重绘之前执行的,并不会造成页面闪动。线上 Demo 请戳这里。
const useParentEffect = useLayoutEffect
// const useParentEffect = useEffect;
function Parent() {
const [v, setV] = useState(1)
useParentEffect(() => {
wait(100)
setV(2)
}, [])
return <Child />
}
function Child() {
const [str, setStr] = useState("111")
useEffect(function create() {
wait(500)
setStr("222")
}, [])
wait(500)
return <div>{str}</div>
}
当 useParentEffect
是 useLayoutEffect
时,页面直接展示 222。
但是当 useParentEffect
是 useEffect
时,或者当注释掉 useParentEffect
的代码时,页面会先展示 111 再展示 222。
总结
本文可总结为以下四点内容。
一、单独就 useEffect
或 useLayoutEffect
而言,会先执行所有的 destroy
回调再执行所有 create
回调。
二、在虚拟 DOM 树中,回调函数的执行顺序如下:
- 对于删除的虚拟 DOM,以先序遍历虚拟 DOM 树的顺序调用
destroy
。 - 对于更新的虚拟 DOM,以后序遍历虚拟 DOM 树的顺序调用
destroy
。 - 以后续遍历虚拟 DOM 树的顺序调用
create
。
三、useEffect
和 useLayoutEffect
的区别在于: useLayoutEffect
是在提交阶段同步执行,而 useEffect
是在未来某时刻执行。
四、useEffect
回调的执行时机为以下两种情况之一:
- 由 React Scheduler 调度,在后续宏任务中执行。
- 在下一次调和阶段之前执行。
推荐更多 React 文章
- React 性能优化 | 包括原理、技巧、Demo、工具使用
- 聊聊 useSWR,为开发提效 - 包括 useSWR 设计思想、优缺点和最佳实践
- React 为什么使用 Lane 技术方案
- React Scheduler 为什么使用 MessageChannel 实现
- 为什么「不变的虚拟 DOM」可以避免组件重新 Render
原创不易,别忘了点赞鼓励哦 ❤️
常见问题FAQ
- 免费下载或者VIP会员专享资源能否直接商用?
- 本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
- 提示下载完但解压或打开不了?
- 找不到素材资源介绍文章里的示例图片?
- 模板不会安装或需要功能定制以及二次开发?
发表评论