在 React 中,useEffect
的依赖数组默认行为确实有一些需要特别注意的地方,尤其是与"浅比较"相关的问题。以下是关键点的解析:
1. 依赖数组的浅比较机制
useEffect
默认使用 严格相等比较(===) 来检查依赖项是否变化:
useEffect(() => {
// effect逻辑
}, [dep1, dep2]); // 比较 dep1 === prevDep1 && dep2 === prevDep2
2. 常见陷阱与解决方案
陷阱1:对象/数组的直接传递
const config = { timeout: 1000 };
useEffect(() => {
const timer = setTimeout(() => {}, config.timeout);
return () => clearTimeout(timer);
}, [config]); // ❌ 每次渲染都会触发(新对象)
解决方案:
// 方法1:拆解基本类型值
useEffect(() => { ... }, [config.timeout]);
// 方法2:useMemo缓存对象
const config = useMemo(() => ({ timeout: 1000 }), []);
陷阱2:函数依赖
const fetchData = () => { /*...*/ };
useEffect(() => {
fetchData();
}, [fetchData]); // ❌ 除非用useCallback
解决方案:
const fetchData = useCallback(() => { /*...*/ }, [deps]);
3. 深度比较的替代方案
如果需要深度比较,可以自定义 hook:
function useDeepCompareEffect(effect, deps) {
const prevDeps = useRef(deps);
if (!deepEqual(prevDeps.current, deps)) {
prevDeps.current = deps;
}
useEffect(effect, [prevDeps.current]);
}
// 使用 lodash 的 isEqual
import { isEqual } from 'lodash';
const deepEqual = isEqual;
4. 性能优化建议
-
依赖项最小化原则:
// 不好 useEffect(() => { ... }, [props]); // 好 useEffect(() => { ... }, [props.id]);
-
空依赖数组:
useEffect(() => { // 仅运行一次的初始化代码 }, []);
-
清理函数:
useEffect(() => { const subscription = source.subscribe(); return () => subscription.unsubscribe(); // 清理上次的effect }, [source]);
5. 为什么React默认使用浅比较?
- 性能考量:深度比较可能代价高昂(O(n)复杂度)
- 确定性:浅比较行为可预测且稳定
- 鼓励不可变数据:推动使用更合理的状态管理方式
实际应用示例
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
// 正确:依赖基本类型值
useEffect(() => {
fetchUser(userId).then(setUser);
}, [userId]);
// 需要传递对象时的处理
const filters = useMemo(() => ({
active: true
}), []);
useEffect(() => {
applyFilters(filters);
}, [filters]);
// ...
}
理解这个机制可以帮助避免不必要的 effect 执行和内存泄漏问题。