useEffect 依赖数组到底怎么用?
这一块儿面试问得很多,今天彻底搞明白
先说结论:useEffect(fn, deps) 什么时候执行,全看 deps 数组怎么写。
三种写法,三种效果
1.useEffect(fn, []) — 空数组
只在两个时候执行:
useEffect(() => {
console.log("我只会执行一次!");
return () => console.log("组件要卸载了");
}, []);
典型场景:接口请求、绑定事件、初始化第三方库。
相当于类组件的 componentDidMount + componentWillUnmount。
2.useEffect(fn) — 不写第二个参数
每次渲染都执行,不管有没有变化。
useEffect(() => {
console.log("每次渲染我都跑");
});
这货基本不用,除非你在调试或者真的需要每次渲染都同步点什么。
3.useEffect(fn, [count, userId]) — 写依赖数组
执行时机:
useEffect(() => {
console.log("count 变了");
}, [count]); // 只有 count 变时才执行
铁律:必须诚实填写依赖
在 effect 里用到的所有响应式数据(state、props、context),都必须填进依赖数组。
忘了填就会出 bug——这就是著名的闭包陷阱。
闭包陷阱是咋回事?
function Counter() {
const [count, setCount] = useState(0);
useEffect(() => {
const timer = setInterval(() => {
console.log(count); // 永远打印 0!
}, 1000);
return () => clearInterval(timer);
}, []); // 空依赖!
return <button onClick={() => setCount(count + 1)}>{count}</button>;
}
因为 [] 空依赖,effect 只在第一次渲染时执行。那时候 count 是 0,定时器闭包捕获的就是这个 0。后面 count 变成 1、2、3... 定时器里还是 0。
怎么解?
写法1:把依赖加上
useEffect(() => {
const timer = setInterval(() => {
console.log(count);
}, 1000);
return () => clearInterval(timer);
}, [count]); // count 变了就重建定时器
缺点:每次 count 变,定时器都要销毁重建,浪费。
写法2:函数式更新(推荐)
useEffect(() => {
const timer = setInterval(() => {
setCount((c) => c + 1); // 不依赖外部变量!
}, 1000);
return () => clearInterval(timer);
}, []); // 稳定了
用 setCount(c => c + 1) 而不是 setCount(count + 1),就不需要依赖 count 了。
写法3:用 ref
function Counter() {
const [count, setCount] = useState(0);
const countRef = useRef(count);
useEffect(() => {
countRef.current = count;
}, [count]);
useEffect(() => {
const timer = setInterval(() => {
console.log(countRef.current); // 永远是最新的
}, 1000);
return () => clearInterval(timer);
}, []);
return <button onClick={() => setCount((c) => c + 1)}>{count}</button>;
}
进阶:依赖是个对象怎么办?
如果你依赖一个对象:
function Child({ config }) {
useEffect(() => {
console.log("config 变了");
}, [config]); // 问题来了!
}
父组件每次渲染,如果不加 useMemo,config 都是新对象:
function Parent() {
const config = { color: "red" }; // 每次渲染都是新对象
return <Child config={config} />;
}
这会导致 useEffect 每次都执行,即使 config 内容没变。
解法
1. 用 useMemo 缓存
function Parent() {
const config = useMemo(() => ({ color: "red" }), []);
return <Child config={config} />;
}
2. 只依赖具体值
useEffect(() => {
console.log("config 变了");
}, [config.color]); // 只依赖具体属性
总结
| 写法 | 什么时候执行 |
|---|
useEffect(fn, []) | 只在挂载时执行一次 |
useEffect(fn, [dep]) | 挂载 + dep 变化时 |
useEffect(fn) | 每次渲染都执行 |
核心原则:在 effect 里用了啥,就诚实地填啥。别想着偷懒用 eslint-disable,不然 bug 找上门。记住,依赖数组就是你和 React 之间的信任。