关于严格模式下useEffect执行两次的问题
# 问题产生
首先来看一段代码
function User(){
useEffect(()=>{
console.log('执行')
},[])
return ({
<div>111</div>
})
}
2
3
4
5
6
7
8
9
10
11
当user组件被渲染的时候 会打印几次?
按照正常逻辑来说,useEffect
的依赖项为一个空数组的时候,他会只在组件被渲染时执行一次。当然,生产环境下确实如此。但如果是在开发环境下,并且恰好你使用了StrictMode
组件包裹你的项目。也就是说你开启了严格模式的情况下。useEffect
会执行两次
。
怎么样,是不是非常的意外。
下面就让我们来深入的了解一下为什么会这样。
# StrictMode
来看看官网对StrictMode
的描述
<StrictMode>
帮助你在开发过程中尽早地发现组件中的常见错误。官方文档 (opens new window)
官网罗列了以下几种用法
- 为整个应用启用严格模式
- 为应用的一部分启用严格模式
- 修复在开发过程中通过双重渲染发现的错误
- 修复在开发中通过重新运行 Effect 发现的错误
- 修复严格模式发出的弃用警告
其他几点先不看,重点在第四点,也就是重新运行Effect这一点上。
每个 Effect 都有一些 setup 和可能的 cleanup 函数。通常情况下,当组件挂载时,React 会调用 setup 代码;当组件卸载时,React 会调用 cleanup 代码。如果依赖关系在上一次渲染之后发生了变化,React 将再次调用 setup 代码和 cleanup 代码。
当开启严格模式时,React 还会在开发模式下为每个 Effect 额外运行一次 setup 和 cleanup 函数。这可能会让人感到惊讶,但它有助于发现手动难以捕捉到的细微错误。
通过官网的描述,我们知道了。在严格模式下useEffect
会被额外
执行一次,刚开始我不是太明白,为什么React要这么做。直到后来我写了这么一串代码。
function Layout(isShow){
const [isLoading,setLoading] = useState(false)
useEffect(()=>{
isLoading = true
setTimeout(()=>{
setLoading(false)
},1000)
},[])
return (
<>
{isLoading&&<Loading/>}
{!isLoading&&<Main/>}
</>
)
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
上面是一个带有loading的组件,初始化展示Loading,等到一定的条件后关闭,上述代码中使用定时器来模拟了一下。试想一下,如果这个组件在很短的时间内发生了两次渲染,那loading就会被打开两次。因为useEffect会跟随组件的渲染被重新执行。这个时候如果是异步请求那么就会发出去两个。这其中有一个肯定是重复的。如果是定时器,也会挂在两个。如果你是用定时器做倒计时功能,是不是就会从-1变成-2了。
上面的例子可能还不够的生动,具体的可以去看React官方提供的例子,非常的形象。
# 总结
那么上面说了这么多,我们可以得出一个结论。React时时刻刻都在提醒你,在使用副作用函数时记得要清除副作用
。也就是说如果你在useEffect
中使用了某些不确定的副作用函数,例如定时器,异步任务等等。这些代码在第一次执行的时候不会有明显的错误发生,但是如果你没掌控好useEffect的执行时机,导致了useEffect被重复执行
。那这个时候你写的那些副作用函数就也会重复执行。导致一些非常难发现的bug。
所以最好在useEffect里写的setup
都提供一个相对应的cleanup
。也就是return一个清除副作用
的函数
将上面的代码改造一下
function Layout(isShow){
const [isLoading,setLoading] = useState(false)
useEffect(()=>{
isLoading = true
const timer = setTimeout(()=>{
setLoading(false)
},1000)
return ()=> {
setLoading(false) // 重置loading的状态
clearTimeout(timer) // 同时清除调之前的定时器
}
},[])
return (
<>
{isLoading&&<Loading/>}
{!isLoading&&<Main/>}
</>
)
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
这样依赖,即使useEffect被莫名其妙的执行了,也能保证你的程序不会出现一些隐藏的错误。
# vue会不会出现这个问题呢?
答案是也会,只不过没有react这么明显。根本原因还是因为两个框架的渲染原理不同导致的。vue触发组件更新是通过劫持的变量发生了改变,然后就回去更新视图。但并不代表整个组件实例会被重新生成。而react则是在变量发生变化后,也就是props或者state变化之后重新调用render,这样一来,就会导致函数组件内的代码会被重复执行。如果把颗粒度放大,就拿定时器来说。在vue中你是不是也经常会在组件卸载时去清除定时器以免造成内存泄漏呢?这和在useEffect里清除副作用其实是一个道理。
# 在react中如何规避这种问题
在react里最好是不要在useEffect
中直接写入异步代码,即使写了也要记得清除副作用
。那如果是调接口的话,最好就不要直接在useEffect
里去调用了,需要又一层包裹来避免。你可以选择手动的去声明变量防止重复
调接口。当然更推荐通过第三方工具来规避这个问题。
强推SWR (opens new window)
他是一个用于请求数据的库,内部集成了非常非常多的小功能,你能想到的基本上他都帮你做了。其中之一就是请求去重。短时间内同时发出的重复请求会被去重处理。这一点是不是就完美的解决useEffect重复执行导致请求重复的问题了。