React useEffect 依赖是数组时长度变化触发执行
React useEffect 依赖数组长度变化触发执行原理与最佳实践
useEffect 依赖项的核心机制
useEffect 的第二个参数是一个依赖数组。React 使用 Object.is 对每一项依赖进行浅比较,以此决定是否跳过本次副作用执行。当数组中任意一个依赖值与上一次渲染时的值不同,effect 会重新运行;如果依赖数组完全省略,则每次渲染后都会执行;传入空数组 [] 则仅执行一次(相当于组件挂载时)。
为什么数组长度变化会触发执行
当依赖项本身是一个数组时,例如:
const [list, setList] = useState([1, 2, 3]);
useEffect(() => {
console.log('list 改变了');
}, [list]); // 依赖项是数组变量 list
触发重新执行的真正原因不是长度变化,而是引用变化。
每次通过 setList 更新状态时,都会创建一个全新的数组引用。React 对比新旧 list 的引用,发现它们不同,因此 effect 重新运行。
长度变化是表象——因为你修改了数组内容(如添加、删除元素),新数组的引用必然改变,从而触发 effect。即使长度相同,只要引用更新(例如 setList([...list]) 在原地复制一份),effect 依然会执行。
典型场景示例
// 向列表添加一项
const addItem = () => {
setList(prev => [...prev, prev.length + 1]);
};
// 数组 length 变化 -> 新引用 -> effect 触发
useEffect(() => {
console.log('列表长度变化或引用变化:', list.length);
}, [list]);
如何精确监听数组长度变化(而不依赖整个引用)
有时你只想在数组长度改变时执行副作用,而忽略内容变化(尽管内容变化通常也伴随着引用变化)。以下方式可以让你更精细地控制:
方法一:直接依赖 array.length
useEffect(() => {
// 仅在 list.length 改变时执行
console.log('长度变为:', list.length);
}, [list.length]);
- 优点:只关注长度,性能更优。
- 注意:如果数组内部元素变化但长度不变,effect 不会执行;这在大多数场景下是期望的行为。
方法二:依赖序列化字符串(需要深度比较时)
如果要在数组内容(元素值)变化时触发,可以将数组转为字符串依赖:
useEffect(() => {
// 数组内容发生任何变化都会执行
}, [JSON.stringify(list)]);
- 优点:深度比较数组内容。
- 缺点:大数组有性能开销,且对象顺序、
undefined等序列化问题需留意。
方法三:使用 useRef 手动追踪上次长度
const prevLengthRef = useRef(list.length);
useEffect(() => {
if (list.length !== prevLengthRef.current) {
// 长度确实变化了才执行逻辑
console.log('长度变化');
prevLengthRef.current = list.length;
}
}, [list]); // 依然依赖 list 引用更新
这可以避免在数组内容变化但长度不变时执行特定逻辑,不过 effect 本身仍会因引用变化而运行,仅在内部做条件判断。
常见误区与注意事项
-
依赖数组内部的对象/数组元素
若依赖项是类似[user.info, items],请注意user.info可能是同一引用但属性更改。React 只比较依赖数组中的值,不会递归比较。如需深度比较,使用序列化或自定义 hook。 -
误用内联数组作为依赖
useEffect(() => { // ... }, [[1, 2, 3]]); // 每次渲染都会创建新数组,导致 effect 每次执行!内联数组每次渲染都是新引用,effect 将无限循环。务必使用稳定的变量引用。
-
函数依赖导致的不必要执行
如果依赖了函数,同样需用useCallback保持引用稳定,避免每次渲染触发 effect。
实战:封装一个监听数组长度变化的 Hook
function useArrayLengthEffect(array, callback) {
const prevLengthRef = useRef(array.length);
useEffect(() => {
if (array.length !== prevLengthRef.current) {
callback(array.length, prevLengthRef.current);
prevLengthRef.current = array.length;
}
}, [array, callback]);
}
// 使用
useArrayLengthEffect(list, (newLength, oldLength) => {
console.log(`长度从 ${oldLength} 变为 ${newLength}`);
});
确保
callback本身用useCallback包裹,避免每次渲染重建导致 effect 重复触发。
总结
useEffect依赖数组采用引用比较,数组变量每次更新都会生成新引用,故 effect 重新执行。- 若需仅响应长度变化,直接依赖
array.length是最简洁可靠的方法。 - 避免依赖内联的新数组或新对象,否则会导致无限循环。
- 在需要更精细控制时,可结合
useRef和条件判断,或封装自定义 Hook 来抽象长度监听逻辑。