创见博客
React之useEffect
七崽爱吃小饼干2026/01/06阅读 0专栏 React

一、什么是副作用(Side Effect)

在 React 中,"副作用"指的是那些不直接参与组件渲染,但需要在组件生命周期中执行的操作,常见的有:

  • 数据请求(接口调用)
  • DOM 操作(比如手动修改元素样式、添加事件监听)
  • 订阅/取消订阅(比如定时器、WebSocket 连接)
  • 本地存储(localStorage/sessionStorage 读写)

useEffect 的作用就是把这些副作用从组件的渲染逻辑中抽离出来,让组件只专注于渲染 UI。

二、useEffect 的基本语法

useEffect 接收两个参数:

jsx
useEffect(effectFn, dependencies);
  1. effectFn(必选):执行副作用的函数,返回值可选(如果返回一个函数,这个函数会作为"清理函数"执行)。
  2. dependencies(可选):依赖数组,决定 effectFn 什么时候执行。

三、useEffect 的核心执行规则

根据依赖数组的不同写法,useEffect 会有完全不同的执行时机,这是最核心的部分:

1. 无依赖数组:组件每次渲染后都执行

jsx
import { useEffect, useState } from 'react';

function Demo() {
  const [count, setCount] = useState(0);

  // 无依赖数组:组件首次渲染 + 每次更新后都会执行
  useEffect(() => {
    console.log('组件渲染/更新了,当前count:', count);
  });

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => setCount(count + 1)}>+1</button>
    </div>
  );
}
  • 执行时机:组件首次挂载到 DOM 后执行一次,之后每次组件重新渲染(比如 count 变化)都会再执行。
  • 注意:这种写法容易导致无限循环(比如在 effect 里修改触发渲染的状态),除非明确需要,否则尽量避免。

2. 依赖数组为空:仅组件首次挂载后执行(模拟 componentDidMount)

jsx
useEffect(() => {
  // 仅在组件第一次渲染完成后执行(挂载阶段)
  console.log('组件挂载完成,只执行一次');
  // 比如这里可以发起初始化数据请求
  fetch('/api/data').then(res => res.json()).then(data => console.log(data));
}, []); // 空数组
  • 执行时机:仅组件首次挂载到 DOM 后执行一次,后续组件更新不会触发。
  • 典型场景:初始化数据请求、添加全局事件监听。

3. 依赖数组有值:仅依赖项变化时执行

jsx
const [count, setCount] = useState(0);
const [name, setName] = useState('React');

// 仅当 count 变化时执行
useEffect(() => {
  console.log('count 变了,当前值:', count);
  // 比如根据 count 变化更新 DOM 或发起请求
}, [count]); // 依赖数组只写 count
  • 执行时机:
    1. 组件首次挂载时执行一次;
    2. 后续只有当依赖数组中的 count 发生变化(前后值不同)时,才会重新执行。
  • 关键:依赖数组要包含 effectFn 中用到的所有组件内变量/状态/函数,否则会导致"闭包陷阱"(拿到旧值)。

四、清理函数(模拟 componentWillUnmount)

effectFn 可以返回一个函数,这个函数就是"清理函数",作用是在副作用执行前(依赖变化时)或组件卸载时执行,用于清理副作用(避免内存泄漏)。

示例:定时器的清理

jsx
function Timer() {
  const [count, setCount] = useState(0);

  useEffect(() => {
    // 副作用:启动定时器
    const timer = setInterval(() => {
      setCount(prev => prev + 1);
    }, 1000);

    // 清理函数:清除定时器
    return () => {
      clearInterval(timer);
      console.log('定时器已清理');
    };
  }, []); // 仅挂载时启动一次

  return (
    <div>
      <p>计时:{count}秒</p>
    </div>
  );
}
  • 清理函数执行时机:
    1. 当组件卸载时(比如 Timer 组件被删除),执行清理函数,清除定时器;
    2. 如果 useEffect 有依赖项,依赖变化时,会先执行上一次的清理函数,再执行新的副作用。

其他清理场景:

  • 取消接口请求(AbortController);
  • 移除事件监听(window.removeEventListener);
  • 关闭 WebSocket 连接。

五、常见坑点 & 最佳实践

  1. 依赖数组不完整: ❌ 错误:effect 中用了 count 但依赖数组没写

    jsx
    useEffect(() => {
      console.log(count); // 依赖了 count,但数组为空
    }, []);
    

    ✅ 正确:依赖数组包含所有用到的变量

    jsx
    useEffect(() => {
      console.log(count);
    }, [count]);
    
  2. 无限循环: ❌ 错误:在 effect 中修改依赖项,且依赖项包含该值

    jsx
    useEffect(() => {
      setCount(count + 1); // 修改 count,而依赖数组包含 count
    }, [count]); // 无限循环
    

    ✅ 解决:用函数式更新(不依赖外部 count)

    jsx
    useEffect(() => {
      setCount(prev => prev + 1); // 不依赖外部 count,依赖数组可空
    }, []);
    
  3. 异步函数的写法: effectFn 不能直接是异步函数(因为会返回 Promise,而非清理函数),需要在内部定义异步函数:

    jsx
    useEffect(() => {
      // 正确:内部定义异步函数
      const fetchData = async () => {
        const res = await fetch('/api/data');
        const data = await res.json();
        console.log(data);
      };
      fetchData();
    
      // 清理函数(如果需要)
      return () => { /* 清理逻辑 */ };
    }, []);
    

总结

  1. useEffect 是 React 函数组件处理副作用的核心 Hook,替代了类组件的 componentDidMount、componentDidUpdate、componentWillUnmount;
  2. 依赖数组决定执行时机:无依赖=每次渲染执行、空依赖=仅挂载执行、有依赖=依赖变化执行;
  3. 清理函数用于清理副作用(定时器、事件监听等),避免内存泄漏,执行时机是依赖变化前/组件卸载时。
评论
0/100