关于useEffect,你可能不知道的细节
前言
有一个 vue 转 react 的同事,说 react 的 useEffect 会执行两次,非常的不科学。为此,我阅读了 react 官网关于 useEffect 的内容,发现蛮多以前没注意的细节,特此记录一下。
useEffect执行细节
useEffect 会接收两个参数,第一个参数我们叫做setup
函数,是处理 Effect 的函数,setup 函数中可以返回一个清理(cleanup
)函数;第二个参数是可选dependencies
,当 dependencies 改变的时候会触发 setup 函数。抛出几个问题大家可以思考一下:
- cleanup 的触发时机?
- dependencies 只能是 props 或者 state 改变后才能触发 setup 函数吗吗?自定义的变量或者 ref 可以吗?
- react 是通过什么方式比较 dependencies 里前后值是否改变,“==”还是"===" or ?
- useEffect 为什么会执行两次?
cleanup触发时机
cleanup 只会在两种情况被触发,第一种:组件卸载的时候;第二种:组件重新渲染的时候。
组件卸载即为:组件从 DOM 中移除之后,比如切换 tab、dispaly 设置为 none 这样的操作。 组件重新渲染时,首先会按顺序执行所有 dependencies 变化的 Effect 的 cleanup 函数,再执行这个 Effect 的 setup 函数。
function Home() {
const [count, setCount] = useState(0);
useEffect(() => {
console.log('setup:b');
return () => {
console.log('cleanup:b');
}
}, [])
useEffect(() => {
console.log('setup:a');
return () => {
console.log('cleanup:a');
}
}, [count])
useEffect(() => {
console.log('setup:c');
return () => {
console.log('cleanup:c');
}
}, [count])
return (
<button onClick={() => setCount(count + 1)} >count:{count}</button>
);
}
点击按钮后打印:
关于dependencies
这里引用官方的的话:
可选
dependencies
:setup
代码中引用的所有响应式值的列表。响应式值包括 props、state 以及所有直接在组件内部声明的变量和函数。
props、state 以及所有直接在组件内部声明的变量和函数都可以作为 dependencies;ref 同样可以,因为对于 react 来说 ref 对象是一个普通对象(这里的 ref 指的是 ref.current)。但是除了 props 和 state 改变会触发 setup 函数,其他 dependencies 改变都不会。
function Home() {
const [count, setCount] = useState(0);
const [otherState] = useState(null);
let obj = { isChanged: false };
function testFunc() { }
const testRef = useRef(0);
useEffect(() => {
console.log('viriable change', obj);
}, [obj])
useEffect(() => {
console.log('func change', testFunc);
}, [testFunc])
useEffect(() => {
console.log('ref change', testRef.current);
}, [testRef.current])
useEffect(() => {
console.log('otherState change', otherState);
}, [otherState])
return (
<>
<button onClick={() => obj = { isChanged: true }} >change viriable</button>
<button onClick={() => testFunc = () => { }} >change func</button>
<button onClick={() => testRef.current = testRef.current + 1} >change ref</button>
<button onClick={() => setCount(count + 1)} >count:{count}</button>
</>
);
}
示例中点击前三个按钮都不会有打印,因为页面没有重新渲染。点击第四个按钮之后,obj
和testFunc
一定会被打印出来,因为 count 改变触发页面重新渲染, obj 和 testFunc 被重新定义,dependencies 改变,触发 setup 函数。如果没有修改 ref 对象,testRef 不会被打印,因为 ref 对象不会因为页面重新渲染而重新定义,相当于这个组件的缓存。对照组 otherState 无论其他变量怎么变化,它也不会被打印。
这里我们稍微展开一下,怎样才能修改 count 之后让 obj 和 testFunc 也不被打印呢?
let obj = useMemo(() => ({ isChanged: false }), []);
let testFunc = useCallback(() => { }, [])
关于第三个问题,react 是通过 Object.is来判断 dependencies 是否变化的。
useEffect为什么会执行两次
实际上 useEffect 只会在 React18 的 dev 环境组件挂载时才会执行 setup 函数两次,执行顺序是 setup -> cleanup -> setup,大可以放心使用,这对生产环境是没有任何影响的。
其目的是对组件做压力测试,针对一些特殊情况可以提早暴露出Bug;比如说有一个1对1的聊天室,在 useEffect 中调用 createConnection 在两个用户之间建立连接,按理说组件卸载之后应该要断开连接,如果没有在 cleanup 中编写断开连接的代码,这里在组件挂载时就能暴露这个问题,可以看看官方的答复。
良好的使用习惯
这里介绍一些常见的使用 useEffect
的良好习惯,如果想要了解更多,移步官网
-
尽量避免依赖组件重复渲染时会重新定义的变量和函数,就像我们示例中的
obj
和testFunc
,可能会导致setup
函数频繁运行。 -
避免根据先前
state
更新state
,有时候我们做轮询操作的时候可能会这样做:
useEffect(() => {
const timer = setInterval(() => {
setCount(count + 1)
}, 1000)
return () => {
clearInterval(timer);
}
}, [count])
或者
useEffect(() => {
setTimeout(() => {
setCount(count + 1)
}, 1000)
}, [count])
这样会导致每次count更新都执行一次 setup 和 cleanup,推荐这样实现:
useEffect(() => {
const timer = setInterval(() => {
setCount((count) => count + 1);
}, 1000)
return () => {
clearInterval(timer);
}
}, [])
- 使用 useEffect 请求数据可能不是最优选,这样做可能会存在以下几个问题:
- Effect 不在服务器上运行,对
SSR
不友好,客户端运行 Effect 时需要下载所有脚本来渲染页面 - 容易造成“网络瀑布”,想象一下渲染父组件会请求数据,子组件依赖父组件的数据渲染页面,然后子组件重复这样的过程在请求它的子组件;如果网络不是很快,这会比并行请求所有数据会慢很多。
- 不会预加载或者缓存数据,可能会频繁的请求数据。
- 不符合工效学,在调用
fetch
时,需要编写大量样板代码,以避免像竞争条件这样的 bug。
- Effect 不在服务器上运行,对
官网推荐两种数据获取方法:现代 React 框架已经很完善了,使用框架内置的数据获取方式;否则考虑使用或构建客户端缓存,流行的开源解决方案包括 React Query、useSWR 和 React Router v6.4+。
- 自定义 hook 封装 Effect,对于一些 Effect 可能在项目中多处都有重复的逻辑或者通用的行为,可以封装成自定义 hook。
总结
关于 useEffect 的细节就写到这里,应该还是有一些东西能用到工作中的。想来距离上一次阅读官网的文档已经记不得有多久了,发现官网改版后阅读体验还不错。在阅读过程中官网还以链接的形式发散了很多其他内容出来,比如服务端渲染相关的、竞争条件这样的bug等,这里都没有展开讲,后续再记录吧。
转载自:https://juejin.cn/post/7388328892309717026