前言
在函数式组件中,useEffect 是处理“副作用”的唯一窗口。无论是数据获取、订阅事件还是手动修改 DOM,都离不开它。理解它的依赖追踪机制和清除逻辑,是规避“无限渲染”黑洞的关键。
一、 核心概念与语法
useEffect 接收一个副作用函数和一个依赖数组。其本质是告诉 React: “在渲染完成后,如果这些值变了,请执行这个函数。”
import React, { useEffect, useState } from 'react';
const EffectDemo: React.FC = () => {
const [count, setCount] = useState<number>(0);
// 语法:useEffect(effectCallback, dependencyArray)
useEffect(() => {
console.log('执行副作用');
// 清理函数(Optional)
return () => {
console.log('执行清理');
};
}, [count]); // 仅在 count 改变时执行
return <button onClick={() => setCount(c => c + 1)}>Count: {count}</button>;
};
export default EffectDemo;
二、 依赖数组的三种境界
| 依赖情况 | 执行时机 | 模拟生命周期 |
|---|---|---|
| 不传参数 | 每次渲染后都执行。容易导致死循环(渲染 -> 状态变 -> 触发渲染)。 | 无 |
空数组 [] | 仅在挂载(Mount)后执行一次。 | componentDidMount |
| 基本类型数组 | 挂载后执行一次,且数组内任一值变化时重新执行。 | componentDidUpdate |
三、 清理函数的触发逻辑
useEffect 的 return 语句定义的函数称为清理函数。其触发分为两种场景:
- 卸载时:如果依赖是
[],仅在组件从 DOM 销毁时执行(模拟componentWillUnmount)。 - 更新前:如果依赖不为空,每次副作用执行前,都会先执行上一次的清理函数,确保旧的订阅或定时器被移除。
四、 深度解析:多 Effect 的执行顺序
假设代码中编写了两个副作用函数,执行顺序为:Effect A -> Effect B,切这两个副作用函数均有依懒项,其执行逻辑如下:
1. 挂载阶段 (Mounting)
- 执行 Effect A 的副作用。
- 执行 Effect B 的副作用。
2. 更新/卸载阶段 (Updating/Unmounting)
注意:清理函数执行顺序不是按照useEffect在代码中的位置来的。
- 执行 Effect B 的清理函数 (Return)。
- 执行 Effect A 的清理函数 (Return)。
- (如果是更新)依次执行新的 Effect A 副作用 -> Effect B 副作用。
注意: 在 React 18 严格模式下,开发环境会执行:挂载 -> 卸载 -> 重新挂载。这是为了帮助开发者尽早发现未清理的订阅或副作用。
五、 避坑指南:无限渲染的成因与对策
1. 引用类型陷阱
问题:依赖数组中包含对象 []、数组 {} 或函数,由于 React 进行的是浅比较(===) ,每次渲染引用地址都不同,会导致无限循环。
解决:
- 使用
useMemo缓存对象/数组。 - 使用
useCallback缓存函数。
2. 依赖项与 SetState 的循环
问题:在副作用中更新状态,且该状态又是该副作用的依赖,会引发无限渲染的问题。
原因:useEffect 运行 -> 状态更新 -> 重新渲染 -> useEffect 再次运行
解决:
- 使用 函数式更新:
setCount(c => c + 1)。这样可以从依赖数组中移除count,打破循环。
// 优化示例
const AvoidLoop: React.FC = () => {
const [count, setCount] = useState(0);
useEffect(() => {
const timer = setInterval(() => {
// ✅ 使用函数式更新,依赖数组可以保持为空
setCount(prev => prev + 1);
}, 1000);
return () => clearInterval(timer);
}, []); // 不需要把 count 放进来
return <div>{count}</div>;
};
export default AvoidLoop;