React useEffect 详解与运用

React组件的生命周期与useEffect

现实应用需要与外部世界交互:获取数据、订阅事件、手动操作DOM等。这些操作在函数式编程中被称为"副作用"(side effects),而useEffect正是处理这些副作用的函数

一、useEffect深度解析

useEffect是React Hooks中最重要的钩子之一,它允许我们在函数组件中执行副作用操作。"副作用"指的是任何会与函数外部世界交互的操作。具体来说,当函数执行时除了返回一个值之外,还做了其他事情,我们就说这个函数产生了副作用。

比如修改DOM,设置定时器,记录日志等。

基本用法

  1. 无依赖数组,没有设置第二个参数,每次渲染后都执行

    jsx 复制代码
      useEffect(() => {
          // 每次渲染后都会执行(包括首次)
        console.log('无依赖数组effect执行')
      })
  2. 空依赖数组,第二个参数为空数组,useEffect仅在挂载时执行

    jsx 复制代码
      useEffect(() => {
        // 只在组件挂载时执行一次
        console.log('空依赖数组effect执行')
      }, []);
  3. 有依赖项,当依赖项变化时执行

    jsx 复制代码
      const [count, setCount] = useState(0);
      useEffect(() => {
        console.log('有依赖项effect执行')
      }, [count]);

    组件首次渲染

    数据count发生改变,第二次渲染,其中空依赖数组effect没有执行

清理机制

useEffect可以返回一个清理函数,这个函数会在组件卸载时执行 ,即当组件从 DOM 中移除时,React 会执行所有 effect 的清理函数;还会在依赖项变化时执行,即当 effect 的依赖项发生变化,React 会先执行上一次 effect 的清理函数,再运行新的 effect

jsx 复制代码
    const [time,setTime] = useState(0)

    useEffect(()=>{
        console.log('定时器开启')
        const timer = setInterval(() => {
            setTime(prevTime => prevTime+1)
        }, 1000)
        return () => {
            console.log('定时器关闭')
            clearInterval(timer)
        }
    },[])

上述例子在 useEffect 中设置了一个每秒更新一次的定时器。如果不添加清理函数,当用户离开这个页面时,组件从DOM树中移除了,但是定时器还在运行。这样不仅浪费系统资源,还会更新以及不存在的页面,导致内存泄漏。如果来回切换更是会使定时器叠加,导致计数异常的问题。

为什么useEffect回调不能直接使用async

useEffect的回调函数不能直接标记为async,因为:async函数会隐式返回一个Promise,而useEffect期望其回调要么不返回任何内容,要么返回一个清理函数。如果返回Promise,React无法正确处理清理逻辑

正确做法是在回调内部使用async函数:

jsx 复制代码
useEffect(() => {
  const fetchData = async () => {
    const result = await someApiCall();
    // 处理结果
  };
  
  fetchData();
}, []);

二、组件生命周期的三个阶段使用useEffect

  1. 挂载阶段(Mounting)

    该节点首先进行组件初始化 ,即创建组件实例,然后运行组件内的所有逻辑代码 ,最后生成虚拟DOM并渲染到实际DOM中

    当组渲染完后useEffect执行,这时我们可以使用useEffect来处理挂载阶段的逻辑,比如发生获取数据的请求。

    jsx 复制代码
    useEffect(() => {
      // 挂载时执行的代码(仅执行一次)
      console.log('组件已挂载');
      console.log('空依赖数组effect执行')
      return () => {
        // 清理函数(将在卸载时执行)
      }
    }, []) // 空依赖数组确保只在挂载时执行
  2. 更新阶段(Updating)

    当组件的props或state发生变化时,组件会进入更新阶段,进入更新阶段后首先会执行组件函数体代码 ,之后会对根据新的状态或属性对组件重新渲染

    这时候可以使用useEffect监听特定状态变化

    jsx 复制代码
    useEffect(() => {
      // 当count变化时执行(包括首次挂载)
      console.log(`Count值已更新为: ${count}`);
      
      return () => {
        // 清理函数(在下次effect执行前或组件卸载时执行)
      }
    }, [count]) // count作为依赖项
  3. 卸载阶段(Unmounting)

    进入卸载阶段,组件从会DOM中移除执行清理操作,如清除定时器、取消网络请求、移除事件监听器等

    使用useEffect的清理函数处理卸载逻辑:

    jsx 复制代码
    useEffect(() => {
      const timer = setInterval(() => {
        console.log('定时器运行中...');
      }, 1000);
      
      return () => {
        clearInterval(timer); // 清除定时器
        console.log('组件已卸载,定时器已清除');
      }
    }, [])

三、具体作用

  1. 组件渲染后 :使用useEffect在组件挂载后请求数据,这样可以避免与渲染争抢资源,先完成初始渲染,再请求数据

    jsx 复制代码
    useEffect(() => {
      const loadData = async () => {
        const data = await fetch('/api/data');
        // 更新状态
      };
      
      loadData();
    }, []);
  2. 考虑竞态条件:在清理函数中取消未完成的请求

    当用户在输入框快速输入"86"一词。输入"8"时发起请求A,输入"86"时发起请求B。**由于网络延迟,可能出现请求B比请求A先返回的情况,最终显示的反而是过时的"r"搜索结果。**这就会导致数据错误。

    我们可以在发生新请求前,将旧请求标记为已经取消的状态来解决这一问题

    jsx 复制代码
    const [query,setQuery] = useState('')
    const [isMounted,setIsMounted] = useState(true)
    useEffect(() => {
      
        setIsMounted(true)
        
      const loadData = async () => {
        const data = await fetch('/api/data');
        if (isMounted) {
          // 更新状态
        }
      };
      
      loadData();
      
      return () => {
        setIsMounted(false);
      };
    }, [query]);

    当发起请求A后再发起请求B,由于网络问题,A的useeffect堵塞在fetch这里,此时B请求已经完成,会调用effect返回的清理函数将isMounted设置为false,若A请求完成后由于isMounted为false所以不能更新数据。

    还可以通过取消未完成的请求来解决这个问题:

    jsx 复制代码
    const [query,setQuery] = useState('')
    useEffect(() => {
      const controller = new AbortController();
        
      const loadData = async () => {
        const res = await fetch('/api/data',{ signal: controller.signal });
        const data = await res.json()
        // 处理逻辑
      };
      loadData()
      
      return () => controller.abort(); // 取消未完成的请求
    }, [query]);
相关推荐
万少几秒前
第五款 HarmonyOS 上架作品 奇趣故事匣 来了
前端·harmonyos·客户端
OpenGL6 分钟前
Android targetSdkVersion升级至35(Android15)相关问题
前端
rzl0222 分钟前
java web5(黑马)
java·开发语言·前端
Amy.Wang23 分钟前
前端如何实现电子签名
前端·javascript·html5
今天又在摸鱼26 分钟前
Vue3-组件化-Vue核心思想之一
前端·javascript·vue.js
蓝婷儿28 分钟前
每天一个前端小知识 Day 21 - 浏览器兼容性与 Polyfill 策略
前端
百锦再30 分钟前
Vue中对象赋值问题:对象引用被保留,仅部分属性被覆盖
前端·javascript·vue.js·vue·web·reactive·ref
jingling55534 分钟前
面试版-前端开发核心知识
开发语言·前端·javascript·vue.js·面试·前端框架
拾光拾趣录39 分钟前
CSS 深入解析:提升网页样式技巧与常见问题解决方案
前端·css
莫空000040 分钟前
深入理解JavaScript属性描述符:从数据属性到存取器属性
前端·面试