一、执行两次的useEffect
前段时间在本地启了一个 React Demo 项目,在编码的过程中遇到一个很奇怪的“Bug”。
入口文件:
1 | // index.tsx |
组件文件:
1 | // App.tsx |
我是万万没想到,就这样几行简单的代码竟然会触发一个“Bug”。
此“Bug”的表现为:
1 | 在 Chrome 控制台里发现 “Hello world!” 被打印了 “两次”。 |
刷新之后依然如此,当时就给我整懵了,第一感觉就是,这怎么可能?
很是纠结一番之后依然没想明白,于是试着去网上搜了一下,发现竟然有人同样遇到过这个问题。
通过网上指引,同时去官网查了一下,终于得出答案。
1 | 这不是 Bug,这是 React18 新加的特性。 |
二、React18 useEffect 新特性
1 | 1.这是 React18 才新增的特性。 |
三、如何应对
看过文档以及了解他们这么做的本意之后,我也能够理解他们会这样做了。
只是,对于这种半强迫式操作多少有些不喜欢,感觉是在代码中”被强迫打一针疫苗?”。
当然,人家就是这么干了,作为 React 的普通使用者,能做的就是 适应它 ,并按照它的规范来做。
1.首先先了解一下 React 中 useEffect 执行的时机
1 | Every time your component renders, React will update the screen and then run the |
每次组件渲染时,React 都会更新页面 UI,然后运行 useEffect 中的代码。
1 | Effects run at the end of the rendering process after the screen updates |
Effect 在屏幕更新之后的 rendering 进程结束的时候执行。
从上面可以得出结论,React 中的 useEffect 执行时机是在组件渲染之后(类似于 window(component).onload ?)。
因此,对于某些“副作用”的渲染,比如异步接口请求,事件绑定等操作我们通常都放在 useEffect 中执行。
当然,useEffect 除了在组件渲染的时候执行外,在组件卸载的时候也有相关执行操作。
在组件卸载的时候会执行 useEffect 方法的return
语句。
1 | useEffect(() => { |
如上代码段,当组件渲染的时候会执行window.a = 100
,当组件卸载的时候会执行window.a = 0
。
知道了 useEffect 的执行时机,也就能明白为什么 React18 中 useEffect 会执行两次了。
1 | 因为, React18 在开发环境中除了必要的挂载之外,还 "额外"模拟执行了一次组件的卸载和挂载。 |
既然知道了原因,那么,接下来就是想办法解决了。
2.怎么样才能让 Effect 执行一次?
对于这个问题,官方文档上面有一句原话:
1 | The right question isn’t “how to run an Effect once,” but “how to fix my Effect |
翻译一下,就是说:
1 | 正确的问题不是“怎么样让 Effect 执行一次”,而是“怎样修复我的 Effect,让它在(重复)挂载之后正常工作” |
也可以理解,毕竟在 React 的未来版本中做离屏渲染的时候 useEffect 肯定会多次执行的。
而且,即使是当前版本,在做页面的前进后退也会面临触发多次 useEffect
。
所以,解决办法其实就是解决 重复挂载卸载之后 应用正常工作了。
3.具体的解决方法
我们知道 useEffect 支持返回一个函数,在组件卸载的时候就会执行该函数。
因此,通常正确解法就是 实现清理函数,并将其在 useEffect 中返回。
当然,不同的 Effect 需要有不同的清理方式。
在常用 Effect 分类下,大致有如下几类清理。
1)清理事件监听
1 | useEffect(() => { |
对于事件监听类函数,在返回函数内部“取消掉事件监听”即可。
2-1)重置页面数据,清理属性状态
1 | useEffect(() => { |
对于一些页面属性的变更,在返回函数内部将其变更的属性进行还原。
2-2)重置页面数据,还原元素状态
1 | import { useEffect, useRef } from 'react'; |
涉及到元素状态的,比如播放器之类,需要对(元素)播放器的状态进行重置。
2-3)重置页面数据,弹窗类。
1 | useEffect(() => { |
如果是默认弹窗类,这种也算是元素状态,同样需要对其(弹出)状态进行重置。
3-1)异步请求页面数据处理,处理异步数据渲染
1 | useEffect(() => { |
如上代码,对于异步请求数据并渲染这一类。
我们可以设置一个 标识位,做到对 请求返回的数据 仅做一次处理与渲染setTodos(json)
。
codesandbox 测试代码段
3-2)异步请求页面数据处理,处理接口请求
上面的方法虽然仅会渲染一次,但是请求依然发起了多次。
如果不希望请求多次,也可以使用请求接口数据的缓存方案,对返回数据进行缓存。
1 | const cache = useRef(null); |
对于异步请求,除了可以处理渲染频率,还可以对接口的请求本身做缓存。
在前面3-1的基础上,缓存接口返回的数据,下次请求的时候如果已经有缓存数据了就直接用,无须再次发起请求。
4)无须清理类
并不是所有的 useEffect 函数都需要清理,对于一些没有副作用的函数,我们完全可以不做处理
1 | useEffect(() => { |
如上代码所示,setZoomLevel 方法仅仅是设置一下 Dom 元素的层级。
这种操作无论同时执行多少次都不会有太大的影响,所以对于这一类我们就随他去吧,毕竟线上也不会执行多次。
5)日志 log 上报类
1 | useEffect(() => { |
对于日志上报类,其实也可以算是无须清理类,但是又有点特殊。
因为,对于日志类,首先在开发环境中我们其实是无须进行上报的,毕竟这种日志打上去也没啥用。
当然,如果是要对上报日志本身这个进行调试等必须上报的情形,这种也有三种应对方式:
1 | 方式一,在本地开发环境使用 console.log 来代替 reportLog。 |
以上就是常见的几类解决 useEffect 多次挂载和卸载所导致副作用的方法。
实际开发过程中肯定不止如上几类,只不过原理都是一样的,我们的最终目标都一致,
那就是想办法消除 useEffect 执行之后的副作用,只要本着这一个目标去做,任何合理方式都是可行的。
四、总结
对于 React18 这种操作确实有点膈应。
但是正如前面所解释的那样,对于未来的离屏渲染或者当前其它会导致重复挂载取消的操作,
如果开发者没处理好确实很可能出现 bug。
因此,深入了解一下 useEffect 执行机制以及解决其副作用的方式还是有必要的。