React Hook 中 useEffect 依赖欺骗与依赖诚实
本文是在:https://juejin.im/post/5ceb36dd51882530be7b1585#heading-13 的基础上进行的探究,非常建议阅读原文
1、声明依赖 []
实现模拟 componentDidMount
在之前写过一篇文章,主要解决在渲染闭包中,如何每次都能拿到当前 某个 state 的最新值,具体文章地址:
在这个过程中,useEffect 依赖了 [count]
,当 count 发生变化的时候,对 ref.current 进行重新赋值
平时在使用的时候,经常会写下面的代码:
useEffect(() => {
console.log('only once');
}, []);
为了模拟所谓的 componentDidMount
在组件挂载的时候只执行一次,在 useEffect 的第二个参数中传入了空数组,告诉 useEffect 有依赖,但是依赖的 state 是空的。
这样 useEffect 只有在第一次渲染之后才会执行一次,后面无论 state 怎么变化触发的 re-render 都不会再次执行 useEffect 中的内容。
虽然大家都在这样用,并且 react 官方也没有禁止这种行为(连一个 warning)都没有,但其实这种一种对 useEffect 的依赖欺骗,虽然指定了要依赖 state 的变更,但是依赖确实空的。
2、依赖声明 []
会产生依赖欺骗的场景
这种依赖欺骗行为其实在某些场景下是有代价的,比如将之前 setTimeout 的场景换成 setInterval,参考下面代码:
useEffect(() => {
const id = setInterval(() => {
setCount(count + 1);
console.log(`[] count is ${count}`);
}, 1000);
return () => clearInterval(id);
}, []);
useEffect(() => {
console.log(`[change] count is ${count}`);
}, [count]);
上面代码中有两个 useEffect,一个依赖了 []
,一个依赖了 count
:
需要注意的是,虽然第一个 useEffect 中显示声明的依赖是 []
,但是本质上里面的代码还是依赖了 count
,这个时候第二个参数写 []
已经是对 useEffect 进行依赖欺骗了
- 上面 useEffect 代码的本意是 useEffect 值执行一次,然后每秒钟执行一次 setCount 并且输出
- 下面 useEffect 的用意是 当 count 发生变化的时候,输出一次 count
其中,useEffect 中 return 需要 return 一个方法,这个方法中可以将一些副作用清楚。
return 返回的匿名函数执行的时机:在本次 useEffect 重新执行的时候,会先执行上一个 useEffect 返回的匿名函数,然后再执行本次 useEffect 代码
现在看下上面的代码返回什么:
从上面的代码中可以看出,最终并没有按照我们想象的方式去执行,最终的结果却是:
- 初始化的时候执行了一次 useEffect,并且执行了一次 setCount
- 后面虽然每次都在执行 useEffect 中的 interval 方法,但是 count 始终是第一次初始化的 0,而 count 自从被 setCount 一次成为 1 之后,就再也不会发生变化。
上面 interval 中出现这种结果其实和之前文章说的 setTimeout 是一样的,第一次 setInterval 中使用的变量 count,永远都在第一次执行 useEffect 的渲染闭包环境中,因此永远都是 0 ,无法继续更新
上面 useEffect 中显然依赖了 count,但是我们却没有对 useEffect 诚实,所以在整个链路上会出现问题
3、在声明了 []
依赖中避免再依赖其他变量
为了解决上面这个问题,我们最好的做法其实是避免对 count 的依赖,也就是通过依赖 []
只执行一次 useEffect 但是在 setInterval 每次都能够拿到最新的 count 进行处理。
同样是上面的方法:
useEffect(() => {
const id = setInterval(() => {
setCount(prevCount => prevCount + 1);
console.log(`[] count is ${count}`);
}, 1000);
return () => clearInterval(id);
}, []);
useEffect(() => {
console.log(`[change] count is ${count}`);
}, [count]);
唯一的改动点其实就是将 setCount(count)
改成了 setCount(prevCount => prevCount + 1)
。
之前的写法中,我们期望的是每次都拿到最新的 count,但是在一个闭包渲染环境中,无法拿到最新的 count,因为依赖欺骗了。
后面的写法中调用的 API 是 setState(prevState => new PrevState)
这种方式,setState 本身支持传入一个 function,并且入参指向当前最新的 state 数值,然后再去依赖最新的 state 产生新的 state。拜托了依赖欺骗,能够实现最终的效果。
4、多个变量依赖解决依赖欺骗带来的问题
上面场景中只有一个 state 依赖,因此只需要 setCount(prevCount => prevCount + 1)
即可,但是如果一次 setState 的时候同时依赖了 两个 state 会发生什么情况呢?
useEffect(() => {
const id = setInterval(() => {
setCount(prevCount => prevCount + step);
setStep(step => step + 1);
console.log(`[] count is ${count}, step is ${step}`);
}, 1000);
return () => clearInterval(id);
}, []);
useEffect(() => {
console.log(`[change] count is ${count}`);
}, [count]);
useEffect(() => {
console.log(`[change] step is ${step}`);
}, [step]);
上面的代码中,setCount 本身的变化依赖了 step
这个 step,而 step 本身虽然也进行了 setStep 变更,但本质上在整个 useEffect 内部还是依赖了 step 的变化。
在这种场景下,即使 step 作为了每次变化,但其实 count 并没有随着 step 的变化而累加步长随之增加。
为了解决这个问题,我们就必须对依赖诚实,但是如果将 step
放在了 useEffect 中,比如下面:
useEffect(() => {
console.log('render useEffect')
const id = setInterval(() => {
setCount(prevCount => prevCount + step);
setStep(step => step + 1);
console.log(`[] count is ${count}, step is ${step}`);
}, 1000);
return () => clearInterval(id);
}, [step]);
上面的代码中通过对 step 进行依赖诚实,可以做到 step 的变化会影响 count 的变化,效果如下:
随之带来的问题也很明显,因为依赖了 step 每次都会重新执行 useEffect,导致最终 setInterval 频繁被实例化,影响性能。
文章版权:Postbird-There I am , in the world more exciting!
本文链接:http://www.ptbird.cn/react-hook-useEffect-useReducer.html
转载请注明文章原始出处 !