React Hooks 编程:深入理解 useEffect 的执行机制与清理副作用

0 阅读5分钟

初学 React 的 Hook 机制,特别是 useEffect 的时候,常常会感到困惑。这篇文章将带你系统梳理 useEffect 的执行机制、依赖项数组的作用,以及如何正确清理副作用。如果你也正在学习 React,希望这篇文章能帮助你少走弯路。

如果有理解不当的地方,还请各位大佬轻声指点,共同进步 😊

🧠 为什么选择函数式编程?

在 JavaScript 中,函数是“一等公民”,这意味着你可以像使用变量一样使用函数:传递、返回、赋值给其他变量。这也为 React 使用函数式组件奠定了基础。

函数就是组件,组件返回 JSX,这就是 React 函数组件的核心思想。

而 React Hooks 就是在这种函数式编程基础上诞生的一组工具,帮助我们在不写类的情况下,也能拥有状态管理和副作用处理的能力。

🔁 useState:管理组件的状态

基本用法:

const [count, setCount] = useState(0)
  • useState 是一个以 use 开头的 Hook(约定),用于在函数组件中添加状态。
  • 它接收一个初始值(如数字、字符串、对象等)。
  • 返回一个数组,包含当前状态值和更新该状态的函数。

示例:

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

  return (
    <div>
      <p>点击次数:{count}</p>
      <button onClick={() => setCount(count + 1)}>点我</button>
    </div>
  )
}

通过 useState,我们实现了组件内部状态的管理,而且不需要使用类组件中的 this.statesetState

⚠️ useEffect:处理副作用

什么是副作用?

在 React 中,副作用是指那些不会直接影响组件返回的 JSX 内容,但会对外部环境产生影响的操作,例如:

  • 请求数据(调用 API)
  • 操作 DOM(如聚焦输入框)
  • 设置定时器或监听事件
  • 订阅外部资源(如 WebSocket)

这些操作不能直接放在组件函数体中同步执行,否则可能导致渲染混乱或者性能问题。

React 提供了 useEffect 这个 Hook,专门用来管理和处理副作用。清理副作用可以帮助开发者管理资源、避免潜在的内存泄漏,以及确保副作用在适当的时机结束或重新初始化。

基本语法:

useEffect(() => {
  // 在这里执行副作用操作
}, [依赖项数组]);

它接受两个参数:

  1. 一个回调函数,用于定义副作用逻辑。
  2. 一个依赖项数组,决定了副作用的执行时机。

用法详解:

1. 没有依赖项数组:每次渲染都执行

useEffect(() => {
  console.log('组件初次渲染和更新时都会执行');
});

这种写法下,只要组件状态发生变化导致重新渲染,副作用就会再次执行。

2. 空数组 []:仅在组件挂载后执行一次

useEffect(() => {
  console.log('仅在组件初次渲染时执行');
}, []);

这是最常见的初始化场景,例如请求数据、订阅全局事件等。

3. 指定依赖项:当依赖项变化时才重新执行

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

useEffect(() => {
  console.log('count 发生变化时执行');
}, [count]);

只有当 count 的值发生变化时,这个副作用才会被触发。

📌 总结三种模式:

依赖项类型是否执行副作用触发时机
不传依赖项✅ 每次组件每次重新渲染
空数组 []✅ 一次组件首次挂载
指定依赖项数组✅ 条件执行当依赖项发生改变时

🧹 清理副作用

有些副作用如果不及时清理,可能会造成内存泄漏或程序异常,例如:

  • 定时器未清除 → 即使组件卸载,还在继续执行
  • 事件监听器未移除 → 多次注册,响应重复
  • 异步请求未取消 → 组件已卸载,结果却试图更新状态

为此,useEffect 允许你在副作用函数中返回一个清理函数,该函数会在以下两种情况下自动执行:

  1. 组件即将卸载时
  2. 当前副作用再次执行前(如果依赖项发生了变化)

✅ 示例:清理定时器

useEffect(() => {
  const timer = setInterval(() => {
    console.log('定时器运行中...');
  }, 1000);

  return () => {
    clearInterval(timer);
    console.log('定时器已清除');
  };
}, []);

在这个例子中:

  • 组件挂载后启动定时器。
  • 组件卸载前,清理函数自动执行,清除定时器。

应用场景示例

场景一:组件挂载时获取数据(空依赖数组)

function App() {
  const [list, setList] = useState([]);

  useEffect(() => {
    async function getData() {
      const res = await fetch('http://example.cn');
      const jsonRes = await res.json();
      setList(jsonRes.data.channels);
    }

    getData();
  }, []);

  return (
    <ul>
      {list.map(item => (
        <li key={item.id}>{item.name}</li>
      ))}
    </ul>
  );
}

📌 特点:只在组件首次渲染时请求数据,适合静态初始化。

场景二:根据状态变化动态获取数据(带依赖项)

const [stateValue, setStateValue] = useState(0);

useEffect(() => {
  getData(stateValue); // 每次 stateValue 变化时执行
}, [stateValue]);

📌 特点:当 stateValue 改变时,重新请求数据,适合动态加载场景。

场景三:监听窗口大小变化并打印状态(闭包陷阱)

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

  useEffect(() => {
    const handle = () => {
      console.log(`当前计数值: ${count}`);
    };

    window.addEventListener('resize', handle);

    return () => {
      window.removeEventListener('resize', handle);
    };
  }, [count]);

  return (
    <button onClick={() => setCount(count + 1)}>增加计数</button>
  );
}

📌 注意:如果不在依赖项中加入 count,那么 handle 函数捕获的是旧的 count 值(闭包陷阱)。添加 [count] 后,确保每次点击按钮都能拿到最新的值。

⚠️ 注意事项与最佳实践

1. 依赖项要完整

如果你在副作用中使用了某个状态或属性,一定要将其加入依赖项数组,否则可能引用到旧值,导致逻辑错误。

❌ 错误示例:

useEffect(() => {
  const handle = () => {
    console.log(count); // 可能是旧值
  };
  window.addEventListener('resize', handle);
}, []); // 忘记添加 count

✅ 正确做法:

useEffect(() => {
  const handle = () => {
    console.log(count); // 永远是最新的值
  };
  window.addEventListener('resize', handle);
  return () => {
    window.removeEventListener('resize', handle);
  };
}, [count]); // 添加依赖项

2. 避免不必要的依赖项

虽然依赖项要完整,但也应尽量减少依赖项数量,以降低副作用执行频率,提升性能。

可以考虑使用 useRef 缓存某些值,或者使用 useCallback 包裹函数,防止因函数地址变化而触发副作用。

3. 清理副作用不是必须的

并不是每个副作用都需要清理。比如简单的数据请求就不需要,因为请求完成就结束了。但如果涉及定时器、事件监听器、长连接等,则必须清理。

📚 结语

useEffect 是 React 函数组件中最强大也是最容易出错的 Hook 之一。掌握它的执行机制和清理方式,不仅能写出更健壮的代码,也能帮助你更好地理解 React 的生命周期模型。