面试官提问:useEffect 的依赖项如果是数组或对象(引用类型),会有什么问题?如何解决?
💡 核心回答技巧
这道题考察的是两个核心点:
- React 依赖检查机制 :使用的是
Object.is(浅比较)。 - 闭包陷阱与死循环:引用类型地址改变导致的不必要渲染或无限循环。
1. 问题的本质:浅比较(Shallow Comparison)
React 在每次渲染后会对比 useEffect 的 deps 数组。 对于基本类型(string, number, boolean),比较的是值 。 对于引用类型(Object, Array, Function),比较的是内存地址。
典型错误场景:
JavaScript
scss
function UserProfile() {
const [count, setCount] = useState(0);
// 每次组件重新渲染,options 都会被重新创建,指向一个新的内存地址
const options = { id: 1 };
useEffect(() => {
console.log('执行了!');
}, [options]); // 即使 options 的内容没变,地址变了,useEffect 就会触发
}
如果 useEffect 内部又触发了状态更新,就会导致无限死循环。
2. 解决方案有哪些?
方案一:使用 useMemo 或 useCallback
这是最正统的方法,将引用类型"持久化"。
useMemo:缓存对象/数组。useCallback:缓存函数。
JavaScript
scss
const options = useMemo(() => ({ id: 1 }), []);
// 只要依赖项不变,options 的引用地址在多次渲染间保持不变
方案二:属性拆解(推荐)
如果对象很大,但我只关心其中的某个属性,直接把该属性放入依赖项。
JavaScript
scss
useEffect(() => {
// 业务逻辑
}, [options.id]); // 监听基本类型 id,而不是整个对象
方案三:使用 useRef
如果这个变量不需要参与渲染(即它的改变不需要触发 UI 更新),可以用 useRef 存储。useRef 返回的是同一个对象引用。
方案四:对于函数,写在 useEffect 内部
如果函数只在效应内部使用,直接移进去,避免将其作为依赖。
3. 进阶追问:如果不小心造成了死循环,该如何排查?
- React DevTools:查看哪个 Hook 的数据一直在跳动。
- 日志大法 :在
useEffect第一行打console.log,看触发频率。
🌟 总结
"
useEffect的依赖项对比执行的是浅比较 。对于引用类型,即使内容一致,只要内存地址改变,效应就会重新执行。解决办法通常是使用useMemo/useCallback冻结引用 ,或者拆解属性,确保只有当真正相关的数据变化时,才触发逻辑。"