本文是在: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 代码

现在看下上面的代码返回什么:

25132-ymvjwqqdis.png

从上面的代码中可以看出,最终并没有按照我们想象的方式去执行,最终的结果却是:

  • 初始化的时候执行了一次 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。拜托了依赖欺骗,能够实现最终的效果。

29727-ihdm31kltv.png

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 的变化而累加步长随之增加。

09793-tv8laweop6.png

为了解决这个问题,我们就必须对依赖诚实,但是如果将 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 的变化,效果如下:

60938-emw55ilz2vc.png

随之带来的问题也很明显,因为依赖了 step 每次都会重新执行 useEffect,导致最终 setInterval 频繁被实例化,影响性能

文章已经结束啦