React useEffect 依赖是数组时长度变化触发执行

FreeGuideOnline 最新 2026-07-06

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 来抽象长度监听逻辑。