likes
comments
collection
share

关于useEffect,你可能不知道的细节

作者站长头像
站长
· 阅读数 39

前言

有一个 vue 转 react 的同事,说 react 的 useEffect 会执行两次,非常的不科学。为此,我阅读了 react 官网关于 useEffect 的内容,发现蛮多以前没注意的细节,特此记录一下。

useEffect执行细节

useEffect 会接收两个参数,第一个参数我们叫做setup函数,是处理 Effect 的函数,setup 函数中可以返回一个清理(cleanup)函数;第二个参数是可选dependencies,当 dependencies 改变的时候会触发 setup 函数。抛出几个问题大家可以思考一下:

  1. cleanup 的触发时机?
  2. dependencies 只能是 props 或者 state 改变后才能触发 setup 函数吗吗?自定义的变量或者 ref 可以吗?
  3. react 是通过什么方式比较 dependencies 里前后值是否改变,“==”还是"===" or ?
  4. 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>
    );
}

点击按钮后打印: 关于useEffect,你可能不知道的细节

关于dependencies

这里引用官方的的话:

可选 dependenciessetup 代码中引用的所有响应式值的列表。响应式值包括 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>
        </>
    );
}

示例中点击前三个按钮都不会有打印,因为页面没有重新渲染。点击第四个按钮之后,objtestFunc一定会被打印出来,因为 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,大可以放心使用,这对生产环境是没有任何影响的。

关于useEffect,你可能不知道的细节 其目的是对组件做压力测试,针对一些特殊情况可以提早暴露出Bug;比如说有一个1对1的聊天室,在 useEffect 中调用 createConnection 在两个用户之间建立连接,按理说组件卸载之后应该要断开连接,如果没有在 cleanup 中编写断开连接的代码,这里在组件挂载时就能暴露这个问题,可以看看官方的答复

良好的使用习惯

这里介绍一些常见的使用 useEffect 的良好习惯,如果想要了解更多,移步官网

  1. 尽量避免依赖组件重复渲染时会重新定义的变量和函数,就像我们示例中的 objtestFunc,可能会导致 setup 函数频繁运行。

  2. 避免根据先前 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);
        }
    }, [])
  1. 使用 useEffect 请求数据可能不是最优选,这样做可能会存在以下几个问题:
    • Effect 不在服务器上运行,对SSR不友好,客户端运行 Effect 时需要下载所有脚本来渲染页面
    • 容易造成“网络瀑布”,想象一下渲染父组件会请求数据,子组件依赖父组件的数据渲染页面,然后子组件重复这样的过程在请求它的子组件;如果网络不是很快,这会比并行请求所有数据会慢很多。
    • 不会预加载或者缓存数据,可能会频繁的请求数据。
    • 不符合工效学,在调用fetch时,需要编写大量样板代码,以避免像竞争条件这样的 bug。

官网推荐两种数据获取方法:现代 React 框架已经很完善了,使用框架内置的数据获取方式;否则考虑使用或构建客户端缓存,流行的开源解决方案包括 React Query、useSWR 和 React Router v6.4+。

  1. 自定义 hook 封装 Effect,对于一些 Effect 可能在项目中多处都有重复的逻辑或者通用的行为,可以封装成自定义 hook。

总结

关于 useEffect 的细节就写到这里,应该还是有一些东西能用到工作中的。想来距离上一次阅读官网的文档已经记不得有多久了,发现官网改版后阅读体验还不错。在阅读过程中官网还以链接的形式发散了很多其他内容出来,比如服务端渲染相关的、竞争条件这样的bug等,这里都没有展开讲,后续再记录吧。

转载自:https://juejin.cn/post/7388328892309717026
评论
请登录