react 的useEffect用法

useEffect 是 React Hooks 中用于处理‌**副作用(Side Effects)**‌的核心钩子。所谓副作用,是指那些会影响组件外部世界或需要在渲染后执行的操作,例如数据获取、订阅事件、手动修改 DOM、设置定时器等。

以下是 useEffect 的详细用法、核心机制及最佳实践。

1. 基本语法

javascript 复制代码
useEffect(() => {
  // 1. 副作用逻辑代码(在组件渲染到屏幕后执行)
  
  return () => {
    // 2. 清理函数(可选):在组件卸载或依赖项变化导致重新执行前调用
  };
}, [dependencies]); // 3. 依赖数组(可选)

它接收两个参数:

  1. ‌副作用函数‌:包含需要执行的逻辑。
  2. ‌依赖数组‌:控制副作用何时重新执行。

2. 三种核心执行模式

通过改变第二个参数(依赖数组),可以精确控制 useEffect 的执行时机:

A. 仅在组件挂载时执行一次(模拟 componentDidMount)

当依赖数组为‌**空数组 []**‌ 时,副作用只在组件首次渲染后执行一次。常用于初始化数据、建立连接等。

javascript 复制代码
useEffect(() => {
  console.log('组件已挂载');
  fetchData(); 
}, []); // 空依赖数组
B. 在特定依赖变化时执行(模拟 componentDidUpdate)

当依赖数组中包含‌具体变量‌时,只有当这些变量的值发生变化(浅比较)时,副作用才会重新执行。

javascript 复制代码
const [userId, setUserId] = useState(1);

useEffect(() => {
  console.log(`用户ID变为: ${userId}`);
  fetchUser(userId);
}, [userId]); // 仅当 userId 变化时执行
C. 每次渲染后都执行

如果‌省略 ‌第二个参数,副作用会在‌每次 ‌组件渲染(包括初始挂载和所有更新)后执行。

注意:这通常会导致性能问题,除非确实需要监听每一次渲染。

javascript 复制代码
useEffect(() => {
  console.log('每次渲染都会执行');
});

3. 清理副作用(Cleanup)

如果副作用涉及资源占用(如定时器、事件监听、WebSocket 订阅),必须返回一个‌清理函数‌。React 会在以下时机调用它:

  1. 组件卸载时。
  2. 依赖项变化,导致副作用重新执行之前(先清理旧的,再执行新的)。

‌示例:管理定时器‌

javascript 复制代码
useEffect(() => {
  const timerId = setInterval(() => {
    console.log('Tick');
  }, 1000);

  // 清理函数:清除定时器,防止内存泄漏
  return () => {
    clearInterval(timerId);
  };
}, []);

‌示例:管理事件监听‌

javascript 复制代码
useEffect(() => {
  const handleResize = () => {
    console.log(window.innerWidth);
  };
  
  window.addEventListener('resize', handleResize);

  // 清理函数:移除监听器
  return () => {
    window.removeEventListener('resize', handleResize);
  };
}, []);

4. 常见陷阱与最佳实践

⚠️ 陷阱一:异步函数直接作为回调

useEffect 的回调函数不能直接声明为 async,因为 async 函数隐式返回一个 Promise,而 useEffect 期望返回 undefined 或清理函数。

‌**错误写法:**‌

javascript 复制代码
// ❌ 错误:useEffect 不支持 async 直接作为回调
useEffect(async () => {
  const data = await fetchData();
}, []);

‌正确写法: ‌

在 useEffect 内部定义并调用异步函数。

javascript 复制代码
// ✅ 正确
useEffect(() => {
  const loadData = async () => {
    try {
      const data = await fetchData();
      setData(data);
    } catch (error) {
      console.error(error);
    }
  };
  loadData();
}, []);
⚠️ 陷阱二:无限循环

如果在 useEffect 中更新了状态,而该状态又是依赖项之一,且没有正确的终止条件,会导致无限循环。

‌**错误示例:**‌

javascript 复制代码
// ❌ 错误:count 更新 -> 触发渲染 -> useEffect 执行 -> count 再次更新...
useEffect(() => {
  setCount(count + 1);
}, [count]); 
⚠️ 陷阱三:遗漏依赖项(Stale Closures)

如果副作用中使用了组件内的变量或函数,但未将其加入依赖数组,可能会导致捕获到旧的值(闭包陷阱)。

‌**建议:**‌

  • 始终将副作用中使用的所有外部变量加入依赖数组。
  • 使用 ESLint 插件 eslint-plugin-react-hooks 自动检查依赖项。
  • 如果依赖项是函数或对象,且每次渲染都重新创建,建议使用 useCallback 或 useMemo 稳定其引用,避免不必要的重复执行。
💡 最佳实践:拆分 Effect

不要试图在一个 useEffect 中处理所有逻辑。应根据不同的关注点拆分为多个 useEffect,每个 Effect 只处理一个独立的副作用。这使得代码更易读、更易维护,且依赖关系更清晰。

javascript 复制代码
// ✅ 推荐:拆分逻辑
useEffect(() => {
  document.title = `User: ${user.name}`;
}, [user.name]);

useEffect(() => {
  const socket = connectToChat(user.id);
  return () => socket.disconnect();
}, [user.id]);

5. React 18 中的行为变化

在 React 18 的‌开发环境 ‌下,如果使用了 <StrictMode>,useEffect 会被‌故意执行两次‌(挂载 -> 执行 -> 清理 -> 再次挂载 -> 再次执行)。

  • ‌目的‌:帮助开发者发现未正确编写清理函数的副作用,确保应用在并发渲染下的稳定性。
  • ‌注意 ‌:生产环境中不会执行两次。确保你的副作用是‌幂等‌的,并且清理函数能正确释放资源。

总结

表格

场景 依赖数组 执行时机
仅初始化 [] 组件挂载后执行一次
响应变化 [dep1, dep2] 挂载后执行一次,且当 dep1 或 dep2 变化时重新执行
每次渲染 无 每次组件渲染后都执行(慎用)

正确使用 useEffect 的关键在于理解它是‌同步组件状态与外部系统‌的桥梁,并通过依赖数组精确控制同步的时机。

相关推荐
涛涛ing1 小时前
2026年的前端:你引以为傲的框架,正在被浏览器原生能力“架空”
前端
计算机魔术师1 小时前
23项AI技术要管了:大模型API还出口得了吗?
前端
凤城老人1 小时前
ZhyCMS成长日记:一个企业 CMS 从 1.0 到 3.1.3 的折腾史
前端
小羊没烦恼!2 小时前
T-SQL查询高级—SQL Server索引中的碎片和填充因子
开发语言·前端·javascript·数据库·后端·python·web安全
定栓2 小时前
Webpack迁到Vite第一节:模块系统迁移:require → import
运维·前端·webpack
小熊在忙fpga2 小时前
N32G430芯片的flash表示
前端·javascript·html
Data analyse4562 小时前
App埋点版本兼容的字段迁移校验
大数据·运维·服务器·前端·数据库·数据分析
NC 王炸2 小时前
AI 做的便签,换个浏览器就没了?加一个导出按钮,把数据带走
javascript·ai编程·数据备份·localstorage·txt导出
todoitbo2 小时前
本来只想给内网开个口子,Natapp 却多给了一道防护
前端·javascript·vue.js·内网穿透·网络防护·natapp