• React18的useEffect会执行两次


    一、执行两次的useEffect。

    前段时间在本地启了一个 React Demo 项目,在编码的过程中遇到一个很奇怪的“Bug”。
    其中简化版的代码如下所示。

    // 入口文件
    import { StrictMode } from 'react';
    import * as ReactDOMClient from 'react-dom/client';
    import App from './App';
    const root = ReactDOMClient.createRoot(document.getElementById('root'));
    root.render(
      
        
      
    );
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    // 组件代码
    import React, { useEffect } from 'react';
    
    const App = () => {
      useEffect(() => {
        console.log('组件挂载完成!');
      }, []);
      return <>Hello world!;
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    我是万万没想到,就这样几行简单的代码竟然会触发一个“Bug”。
    此“Bug”的表现为:在 Chrome 控制台里发现 “Hello world!” 被打印了 “两次”。
    刷新之后依然如此,当时就给我整懵了,第一感觉就是,这怎么可能?
    很是纠结一番之后依然没想明白,于是试着去网上搜了一下,发现竟然有人同样遇到过这个问题。

    通过网上指引,同时去官网查了一下,终于得出答案。

    这不是 Bug,这是 React18 新加的特性。
    
    • 1

    二、React18 useEffect 新特性

    1.这是 React18 才新增的特性。
    2.仅在开发模式("development")下,且使用了严格模式("Strict Mode")下会触发。
      生产环境("production")模式下和原来一样,仅执行一次。
    3.之所以执行两次,是为了模拟立即卸载组件和重新挂载组件。
      为了帮助开发者提前发现重复挂载造成的 Bug 的代码。 
      同时,也是为了以后 React的新功能做铺垫。 
      未来会给 React 增加一个特性,允许 React 在保留状态的同时,能够做到仅仅对UI部分的添加和删除。
      让开发者能够提前习惯和适应,做到组件的卸载和重新挂载之后, 重复执行 useEffect的时候不会影响应用正常运行。
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    如何应对

    看过文档以及了解他们这么做的本意之后,我也能够理解他们会这样做了。
    只是,对于这种半强迫式操作多少有些不喜欢,感觉是在代码中”被强迫打一针疫苗?”。

    当然,人家就是这么干了,作为 React 的普通使用者,能做的就是 适应它 ,并按照它的规范来做。

    1.首先先了解一下 React 中 useEffect 执行的时机

    Every time your component renders, React will update the screen and then run the
    code inside useEffect.
    
    • 1
    • 2

    每次组件渲染时,React 都会更新页面 UI,然后运行 useEffect 中的代码。

    Effects run at the end of the rendering process after the screen updates
    
    • 1

    Effect 在屏幕更新之后的 rendering 进程结束的时候执行。

    从上面可以得出结论,React 中的 useEffect 执行时机是在组件渲染之后(类似于 window(component).onload ?)。
    因此,对于某些“副作用”的渲染,比如异步接口请求,事件绑定等操作我们通常都放在 useEffect 中执行。

    当然,useEffect 除了在组件渲染的时候执行外,在组件卸载的时候也有相关执行操作。
    在组件卸载的时候会执行 useEffect 方法的return语句。

    useEffect(() => {
      window.a = 100;
      return (window.a = 0);
    }, []);
    
    • 1
    • 2
    • 3
    • 4

    如上代码段,当组件渲染的时候会执行window.a = 100,当组件卸载的时候会执行window.a = 0。

    知道了 useEffect 的执行时机,也就能明白为什么 React18 中 useEffect 会执行两次了。

    因为, React18 在开发环境中除了必要的挂载之外,还 "额外"模拟执行了一次组件的卸载和挂载。
    
    • 1

    既然知道了原因,那么,接下来就是想办法解决了。

    2.怎么样才能让 Effect 执行一次?。

    对于这个问题,官方文档上面有一句原话:The right question isn’t “how to run an Effect once,” but “how to fix my Effect so that it works after remounting”.翻译一下,就是说:正确的问题不是“怎么样让 Effect 执行一次”,而是“怎样修复我的 Effect,让它在(重复)挂载之后正常工作”
    也可以理解,毕竟在 React 的未来版本中做离屏渲染的时候 useEffect 肯定会多次执行的。
    而且,即使是当前版本,在做页面的前进后退也会面临触发多次 useEffect。

    所以,解决办法其实就是解决 重复挂载卸载之后 应用正常工作了。
    ###3.具体的解决方法
    我们知道 useEffect 支持返回一个函数,在组件卸载的时候就会执行该函数。
    因此,通常正确解法就是 实现清理函数,并将其在 useEffect 中返回。
    当然,不同的 Effect 需要有不同的清理方式。

    在常用 Effect 分类下,大致有如下几类清理。

    1)清理事件监听
    useEffect(() => {
      function handleScroll(e) {
        console.log(e.clientX, e.clientY);
      }
      window.addEventListener('scroll', handleScroll);
      return () => window.removeEventListener('scroll', handleScroll);
    }, []);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    对于事件监听类函数,在返回函数内部“取消掉事件监听”即可。

    2-1)重置页面数据,清理属性状态
    useEffect(() => {
      const node = ref.current;
      node.style.opacity = 1; // Trigger the animation
      return () => {
        node.style.opacity = 0; // Reset to the initial value
      };
    }, []);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    对于一些页面属性的变更,在返回函数内部将其变更的属性进行还原。

    2-2)重置页面数据,还原元素状态
    import { useEffect, useRef } from 'react';
    
    function VideoPlayer({ src, isPlaying }) {
      const ref = useRef(null);
    
      useEffect(() => {
        if (isPlaying) {
          ref.current.play();
        } else {
          ref.current.pause();
        }
      });
    
      return 
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    涉及到元素状态的,比如播放器之类,需要对(元素)播放器的状态进行重置。

    2-3)重置页面数据,弹窗类。
    useEffect(() => {
      const dialog = dialogRef.current;
      dialog.showModal();
      return () => dialog.close();
    }, []);
    
    • 1
    • 2
    • 3
    • 4
    • 5

    如果是默认弹窗类,这种也算是元素状态,同样需要对其(弹出)状态进行重置。

    3-1)异步请求页面数据处理,处理异步数据渲染
    useEffect(() => {
      let ignore = false;
      async function startFetching() {
        const json = await fetchTodos(userId);
        // 这里执行是异步的,所以第一次执行到此处的时候组件已经被卸载了
        // 此时的 ignore 已经被 return 里面的方法置为 true 了
        // 所以这里第一次执行的时候不执行 setTodos(json)
        // setTodos 其实是在第二次执行的时候才触发
        if (!ignore) {
          setTodos(json);
        }
      }
      startFetching();
    
      return () => {
        ignore = true;
      };
    }, [userId]);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    如上代码,对于异步请求数据并渲染这一类。
    我们可以设置一个 标识位,做到对 请求返回的数据 仅做一次处理与渲染setTodos(json)。
    codesandbox 测试代码段

    3-2)异步请求页面数据处理,处理接口请求

    上面的方法虽然仅会渲染一次,但是请求依然发起了多次。
    如果不希望请求多次,也可以使用请求接口数据的缓存方案,对返回数据进行缓存。

    const cache = useRef(null);
    useEffect(() => {
      let ignore = false;
      async function startFetching() {
        if (!cache.current) {
          cache.current = await fetchTodos(userId);
        }
        if (!ignore) {
          setTodos(cache.current);
        }
      }
      startFetching();
    
      return () => {
        ignore = true;
      };
    }, [userId]);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    对于异步请求,除了可以处理渲染频率,还可以对接口的请求本身做缓存。
    在前面3-1的基础上,缓存接口返回的数据,下次请求的时候如果已经有缓存数据了就直接用,无须再次发起请求。

    4)无须清理类

    并不是所有的 useEffect 函数都需要清理,对于一些没有副作用的函数,我们完全可以不做处理

    useEffect(() => {
      const map = mapRef.current;
      map.setZoomLevel(zoomLevel);
    }, [zoomLevel]);
    
    • 1
    • 2
    • 3
    • 4

    如上代码所示,setZoomLevel 方法仅仅是设置一下 Dom 元素的层级。
    这种操作无论同时执行多少次都不会有太大的影响,所以对于这一类我们就随他去吧,毕竟线上也不会执行多次。

    5)日志 log 上报类
    useEffect(() => {
      reportLog({ name: 'viewCount' });
    }, []);
    
    • 1
    • 2
    • 3

    对于日志上报类,其实也可以算是无须清理类,但是又有点特殊。
    因为,对于日志类,首先在开发环境中我们其实是无须进行上报的,毕竟这种日志打上去也没啥用。
    当然,如果是要对上报日志本身这个进行调试等必须上报的情形,这种也有三种应对方式:

    方式一,在本地开发环境使用 console.log 来代替 reportLog。
    方式二,取消掉严格模式(StrictMode) 方式三,构建一个 production
    版本启动,或者将其部署到 QA 环境,部署的时候,指定 production 模式。
    
    • 1
    • 2
    • 3

    借鉴链接:大神地址:epoos

  • 相关阅读:
    netperf 测试时延和吞吐
    freeswitch服务器的时间同步设置
    Springboot老年人社区服务平台设计与实现x45mt计算机毕业设计-课程设计-期末作业-毕设程序代做
    uni-app项目自动化测试
    VsCode连接远程服务器并修改文件代码
    Oracle 表连接(内连接、外连接(左连接、右连接)、隐式连接、表并集、表交集、表补集)
    【云原生】K8s pod 动态弹性扩缩容 HAP(metrics-server)
    Docker安装nginx+php
    c++入门
    敏捷技巧:产品经理的需求文档应该写成什么样才算合格?
  • 原文地址:https://blog.csdn.net/qq_34164814/article/details/127750672