所谓 React 中的闭包陷阱(Closure Trap),其实是 函数组件里的事件回调或副作用中,引用了过期的 state 或 props,导致逻辑异常。


1. 为什么 React 里会有闭包陷阱?

  • React 函数组件 本质上是一个 render 函数
  • 每次渲染都会生成新的 props / state,以及新的 函数作用域
  • 如果某个回调函数(比如事件处理函数、setTimeout 里的回调、useEffect 里的函数)引用了旧的 state/props,就会形成一个 闭包,而这个闭包“锁死”了旧的值。

2. 典型例子

import { useEffect, useState } from "react";

export default function Counter() {
  const [count, setCount] = useState(0);

  useEffect(() => {
    let id = setInterval(() => {
      setCount(count + 1);
    }, 1000);
    return () => clearInterval(id);
  }, []);

  return (
    <div>
      <p>{count}</p>
    </div>
  );
}

页面只会从 0 → 1,之后 不再递增

useEffect([]) 只在首次渲染执行一次,此时 count === 0

setInterval 回调里 闭包捕获的是“第一次 render 时的 count

后续 render 虽然 count 变了,但 interval 里的 count 永远是 0

useEffect 只会在第一次 commit 时候执行回调,后续继续使用 count === 0 ,然后 0 + 1 这个旧值,所以我们要么手动添加 count 依赖项,要么 通过回调函数的 setState 方式修改,不断获取最新的 state。

👉 本质:副作用函数中使用了旧状态


3. 解决办法

✅ 方法一:使用函数式更新(推荐)

setCount(prev => prev + 1);
  • prevReact 在 render 时传入的最新 state
  • 不依赖闭包
  • 并发安全

✅ 方法二:依赖数组正确写法(useEffect

  useEffect(() => {
    let id = setInterval(() => {
      setCount(count + 1);
    }, 1000);
    return () => clearInterval(id);
  }, [count]);

👉 保证每次 count 变了,副作用都能捕获到新值。

问题:

  • 每次 count 变化
  • 都会清除 + 重建 interval
  • 性能和语义都不好

✅ 方法三:使用 useRef 保存最新值

function Counter() {
  const [count, setCount] = useState(0);
  const countRef = useRef(count);

  countRef.current = count;

  useEffect(() => {
    const id = setInterval(() => {
      setCount(countRef.current + 1);
    }, 1000);

    return () => clearInterval(id);
  }, []);

  return <h1>{count}</h1>;
}

👉 ref 不会因为重新渲染而丢失,适合存放“跨渲染保持的最新值”。适合复杂场景(如 WebSocket / 事件监听),但简单计数器没必要


4. 总结

  • 闭包陷阱原因:函数组件每次渲染都会创建新的作用域,回调函数可能捕获旧的 props/state。

  • 常见场景setTimeoutsetInterval、事件回调、异步请求回调、useEffect

  • 解决办法

    1. 函数式更新 setState(prev => ...) (推荐!)
    2. 正确声明依赖(useEffect
    3. 使用 useRef 存最新值

5. 从底层来看这个问题

第一次 render(count = 0)

const [count, setCount] = useState(0);
  • count = 0
  • React 创建 Fiber
  • effect 被注册(但还没执行)

页面显示:

<p>0</p>

commit 阶段 → 执行 useEffect

useEffect(() => {
  let id = setInterval(() => {
    setCount(count + 1);
  }, 1000);
}, []);

⚠️ 注意:

  • count第一次 render 的快照
  • 此时:count === 0

等价于:

setInterval(() => {
  setCount(0 + 1);
}, 1000);

1 秒后,interval 第一次执行

setCount(1);

React 内部:

  • enqueue update
  • 触发 render

第二次 render(count = 1)

<p>1</p>

⚠️ 但注意:

  • effect 不会重新执行
  • interval 里的回调 还是老的闭包

第 2 秒、第 3 秒、第 N 秒……

每一次 interval 执行的都是:

setCount(0 + 1);

React 发现:

  • state 已经是 1
  • 再 set 1 → 无变化
  • React 会 bail out(跳过更新)

📌 所以 count 卡死在 1

闭包捕获的是 render 时的快照,而不是 Fiber 上的最新 state

深入分析

const count = hook.memoizedState; // 当前 render 的值
  • 每次 render 都是新的 count
  • effect 中的回调,只绑定了一次 render 的 count
useEffect(..., []);

等价于:

“我保证 effect 不依赖任何 render 中的变量”

但实际上你偷偷依赖了 count

为什么 React 不帮你“自动更新 count”?

因为:

  • render 可以被并发中断
  • effect 回调可能被丢弃
  • React 不能在 effect 中隐式追踪变量变化

📌 这就是 Hooks 要求你显式声明依赖的原因

更多推荐