回顾React之useEffect && thinking

309 阅读10分钟

useEffect

官网地址:useEffect - React

翻译自官网并在翻译的时候记录下自己的思考;

useEffect是最常用的React Hook了,几乎每一个页面都会用到;因为他可以模拟类式组件的生命周期;

useEffect(setup, dependencies?)

setup:第一个参数是一个函数,用来编写需要执行的逻辑;也可以返回一个清理函数(如果没有要清理的逻辑可以不写);当组件被添加到DOM时,React会执行该函数,或者当依赖项改变时,先执行清理函数然后使用新的值执行我们写的逻辑;在组件被移除的时候也会执行一次清理函数;

dependencies:第二个参数可传可不传;是一个参数是否执行的依赖项,是一个数组;如果传空数组的话,就可以模拟组件加载的生命周期,只会在组件第一次加载的时候执行; dependencies:第二个参数可传可不传;是一个参数是否执行的依赖项,是一个数组;如果传空数组的话,就可以模拟组件加载的生命周期,只会在组件第一次加载的时候执行;

  1. 第二个参数不传

    但是,刚刚看官网的时候,我才发现,第二参数的后面带着问号,那也就是说可以不传呗,试了一下果然是不传也可以;

    image.png

    试了一下发现只要组件中的Reactive values(官网的形容词:包含props,state,以及在组件体内所有的变量和函数)值发生变化,他都会执行;

    If you omit this argument, your Effect will re-run after every re-render of the component.

    官网原话:如果你省略第二个参数,你的effect函数会在每次组件渲染的时候都会执行;

    😂:官网还是要认真👀呢!!

    但是什么场景下使用这种方式呢?

  2. 有依赖项和清理函数的useEffect

    在开发中,我从来没有使用过这种useEffect,原来依赖项改变的时候,会先去执行清理函数,然后在执行;

    const [inputVal, setInputVal] = useState('')
    useEffect(() => {
      console.log('没有第二个参数的useEffect')
    })
    useEffect(() => {
        console.log(`依赖${inputVal}的useEffect`)
        return () => {
          console.log(`依赖${inputVal}的useEffect的清理函数`)
        }
      }, [inputVal])
    

    image.png

    当我改变inputVal的时候,先执行清理函数,然后自上而下顺序执行useEffect;

  3. 无依赖项有清理函数的useEffect

    这种还是比较常用的,比如说在组件卸载的时候清理掉组件中的定时器;

    useEffect(() => {
        let timer: any = setInterval(() => {
          console.log('当前时间:', dayjs().format('YYYY-MM-DD HH:MM:ss'))
        }, 1000)
        return () => {
          timer = null
        }
      }, [])
    
  4. 依赖组件内定义的对象变量的useEffect

    // 依赖组件内的对象
      const options = { name: '我是一个对象' }
    
      useEffect(() => {
        console.log('我是依赖组件内对象的useEffect')
      }, [options])
    
    // 依赖handlebtnClick函数的useEffect
      const handlebtnClick = () => {
        console.log('按钮的点击事件')
      }
    
      useEffect(() => {
        console.log('依赖handlebtnClick事件的useEffect')
      }, [handlebtnClick])
    

    image.png

    组件内部定义一个常量对象,然后创建一个依赖该对象的,组件的每一次重新渲染都会执行,const options = { name: '我是一个对象' }因为每一次重新渲染都会执行,重新为对象赋值;所以尽量避免这样使用(函数同理);

    Avoid using an object created during rendering as a dependency. Instead, create the object inside the Effect:

    官方给的建议是避免在组件渲染的时候定义对象;

    😳:那如果就是本组件需要在页面展示的一些常量数据呢???比如下拉框的数据列表;

使用警告(在自己的理解上翻译的官网🐶(狗头保命)):

  1. useEffect最好放在组建内部使用,不要放在循环或者条件里使用,如果必须放在循环里面,最好将它提到新的组件里去使用;

    😳:不理解放在循环或者条件里使用是什么场景???

  2. 如果你不需要将某些逻辑的执行与页面保持同步,那么就代表你不需要使用该hook;

  3. 在严格模式下,React会在第一次执行setup时,先执行一次setup和清理函数;这是一种确保你的清理函数和启动逻辑的映射关系以及在停止或者撤销启动逻辑正在执行的时候的一种压力测试。如果造成了什么问题,就会执行清理函数;

  4. 如果你的依赖项中包含组件内定义的对象或者函数,它可能会造成比你需要的要更频繁的去执行该****Effect;****为了修复这个问题,将非必要的对象和函数依赖删掉;你也可以在函数外部提取新的更新状态和非响应的逻辑;

  5. 如果你的Effect不是由页面交互引起的,比如说点击事件,那么react会在执行你的Effect之前重绘页面;如果你的Effect是做一些视觉上的效果,比如定位tooltip,并且有一定的延迟或者视觉的闪烁,建议将你的useEffect 替换为[useLayoutEffect](<https://react.dev/reference/react/useLayoutEffect>)

    😳:我还真没这么使用过,useLayoutEffect我也没使用过;

  6. 甚至,如果你的Effect是被像click这样的交互引起的,浏览器也许会在你的Effect内部状态值更新之前重新绘制屏幕,除非,那是你想要的;然而,如果你必须阻止浏览器重绘屏幕,你需要将你的useEffect 替换为useLayoutEffect

  7. Effects只能运行在客户端,不能用于服务端渲染;

自定义hook

自定义hook主要利用的就是useEffect与组件内部的useState配合使用;

如果你发现在你自己老是写类似功能的useEffect,你就可以将这部分Effect提取出来,放到自定义的hook里面;

从Effect中读取最新的状态值和属性值

关于官网中说在useEffect中获取并使用最新的state值,正在开发中还未发布;但是它随后举的关于useEffectEvent这个例子我不是很赞成,总觉得没那么恰当;

官网代码:

function Page({ url, shoppingCart }) {

const onVisit = useEffectEvent(visitedUrl => {

logVisit(visitedUrl, shoppingCart.length)

});

useEffect(() => {

onVisit(url);

}, [url]); // ✅ All dependencies declared

// ...

}

场景大概就是希望只有props中的url变化的时候才执行****Effect,****而不是shoppingCart变化的时候也执行;我觉得依赖项中只依赖url就可以实现了;

当然,你可能会说,代码会报错,提示你Effect中用到了shoppingCart,必须要依赖它才可以;但是,这个跟你的代码检查工具的配置有关,而且在业务开发中是很常见的,所以几乎不会配置你的Effect中用到的活性值就必须要依赖它这个要求;

而且,在这一点上比较容易出错的点是,在我们的Effect中设置了某个状态值并且接着调用了其他的函数,正好该函数中也用到了该状态值;就容易出现在该函数中直接用该state,而这时的state值还是旧的,并不是刚刚set的;

我理解的是这种场景,也可能我理解错了吧😅!

故障排除

  1. 我的Effect,在组件加载的时候执行两次

    严格模式下并且在开发环境中,react会在真实的setup执行前,先执行一下setup和cleanup;

    react官方解释说这么做会帮我们发现bug;

    但我这里要补充一点,差不多类似的现象但不是这个问题造成的;

    问题出现的背景:编写的Effect依赖某个活性值改变的时候,但是活性值在组件声明的时候值为空,然后组件加载的时候执行了某个Effect,set了新的值;这时候会导致依赖该状态值的Effect在值为空的时候调用了一次,赋值完又执行了一次,特别如果涉及到从服务端调接口的时候就更明显了;

    其实这种就说明我们的业务逻辑写的有点问题,或者可以加个判断,当状态值为空(或者某个特殊值的时候)不执行,具体自己根据业务逻辑调整;

  2. 我的Effect,每一次组件渲染都会执行

    第一个原因:查看你的Effect有无依赖项,没有传第二个参数依赖数组的话,组件每一次渲染都会被执行;

    第二个原因:你的依赖数组肯定有某一个依赖项每次渲染的时候在改变,可以debug一下你的依赖项;

  3. 我的Effect,无限循环

    如果你的Effect一直无限循环执行,那么肯定存在以下问题:

    • 你的Effect里面更新了某些状态值;
    • 且,你的Effect的依赖项里含有该状态值,然后便导致你的Effect无限循环执行;

    在你开始修复这个问题之前,问一下你自己,是否你的Effect需要连接外部系统(比如页面的DOM结构,网络,第三方的库等等)。为什么你的Effect需要设置状态?它需要和外部的系统保持同步吗?或者说,它正在试图管理你的应用的数据吗?

    如果你的Effect里不需要连接外部系统,那你需要考虑是否完全移除你的Effect,并且简化你的逻辑。

    😳:我觉得这个思考真的很重要,就是你为什么使用useEffect,只有满足这些条件,才证明你的useEffect使用的是没问题的,在项目review的时候,有时候就会看到一些useEffect的滥用;

    如果你确实需要跟外部系统保持同步,那么就思考一下你的Effect里为什么要更新这个状态值以及更新这个状态的条件;是否有什么改变影响了组件的视觉输出?如果你需要跟踪一些不需要组建渲染使用的数据,那么使用ref(不会触发页面渲染)也许更合适;所以你更需要验证你的Effect不需要改变状态或者触发组件渲染;

    最后,如果你的Effect是在正确的时间更新状态的,但是仍然处于循环中,那可能是由于别的Effect引起的;

  4. 即使我的组件没有卸载,我的清理逻辑也执行了

    清理逻辑不仅仅是在组件卸载的时候执行,在依赖项更新之前也会执行;另外,在开发环境严格模式下,react在组件在启动逻辑执行之前也会调用并执行一次启动逻辑+清理逻辑;

    如果你的清理逻辑没有相应的启动逻辑,类似下面这种:

    useEffect(() => {
    // 🔴 Avoid: Cleanup logic without corresponding setup logic
    
    return () => {
    
    doSomething();
    
    };
    
    }, []);
    

    😳: 其实这种代码在项目中是很常见的,甚至我自己有时候也会写,因为我想的是依赖项是空数组,那么这个代码便是只在组件卸载的时候才会执行了;再看下面官网给的例子:

    你的清理逻辑应该和启动逻辑时对称的:

    useEffect(() => {
        const connection = createConnection(serverUrl, roomId);
        connection.connect();
        return () => {
          connection.disconnect();
        };
      }, [serverUrl, roomId]);
    

    😳: en~,以后再写清理函数的时候要注意一下,保持”对称“。

  5. 我的Effect是做一些视觉上的效果,并且在它运行之前我看到了闪烁

    如果你的Effect必须阻塞浏览器重绘页面,使用[useLayoutEffect](<https://react.dev/reference/react/useLayoutEffect>)代替useEffect;注意,对于大多数的Effect来说这不是必须的;只有在浏览器重绘页面之前执行你的Effect非常重要时,你在需要这么做;比如在你看到tooltip之前计算他的展示位置;

😊:在有了一定开发经验,回过头来再继续看一下官网,对你平时遇到的很多问题会恍然大悟,因为刚开始用的时候,肯定有一些理解的不是很深刻,然后回过头看的时候,会发现之前困扰自己的一些问题,好多都是自己使用不当造成的!(🐶只针对我自己而言)