聊聊那个让 React 新手抓狂的“闭包陷阱”:Count 为什么永远是 0?

写 React Hooks 的时候,你有没有遇到过这种"灵异事件":

明明天在这个组件里 setCount 已经加到飞起了,界面上的数字也在跳动,但是 setInterval 或者是 useEffect 里的 console.log 打印出来的,却永远是初始值 0

这时候你会怀疑人生:"是我眼花了,还是 React 坏了?"

其实 React 没坏,你只是掉进了**"闭包陷阱" (Stale Closure)**。今天咱们就借一段简单的代码,扒一扒这个坑的底裤,顺便看看怎么优雅地爬出来。

案发现场:诡异的"时间冻结"

让我们先看看这段经典的"受害者"代码。这是很多同学(包括刚开始写 Hooks 的我)都会写出的逻辑:

JavaScript

javascript 复制代码
import { useEffect, useState } from "react";

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

  // ❌ 这是一个典型的闭包陷阱现场
  useEffect(() => {
    const timer = setInterval(() => {
      // 这里的 count 永远是 0,仿佛时间被冻结了
      console.log('Current count:', count); 
    }, 1000);

    return () => clearInterval(timer);
  }, []); // 👈 罪魁祸首在这里:空依赖数组

  return (
    <>
      <p>{count}</p>
      <button onClick={() => setCount(count + 1)}>count + 1</button>
    </>
  );
}

现象描述

当你运行这段代码,点击按钮让 count 增加时:

  1. 界面(UI) :显示 1, 2, 3... (正常更新,说明 State 确实变了)。
  2. 控制台(Console)Current count: 0 ... Current count: 0 ... (像复读机一样)。

为什么会这样?

要理解这个问题,首先要修正一个心智模型:每一次渲染(Render),都是一次独立的"快照"。

  1. 第一次渲染 (Mount)

    • React 创建了组件,此时 count = 0
    • useEffect 执行。因为它依赖是 [],所以它只在第一次渲染时执行
    • setInterval 被创建。关键点来了: 这个定时器的回调函数是在 count0 的那个闭包作用域里定义的。它捕获了那一刻的 count(也就是 0)。
  2. 第二次渲染 (点击按钮后)

    • React 再次执行组件函数,count 变成了 1
    • 但是! useEffect 的依赖数组是空的,React 认为"没必要重新运行这个 Effect"。
    • 于是,那个旧的 定时器(Mount 时创建的)依然在坚强地活着。它手里紧紧攥着的,依然是第一次渲染时的旧变量 0

简单来说:你的组件 UI 已经活在 2026 年了,但那个定时器还活在 2023 年,它根本不知道外面的世界变了。这就是 JS 词法作用域与 React Hooks 机制碰撞出的"火花"。


怎么爬出陷阱?

既然知道了是因为"引用了旧变量",那想要实现如下图片效果,思路就很清晰了:要么让 Effect 重新执行,要么用某种方式穿透闭包。

方法一:诚实地告诉 React 你的依赖(官方推荐)

这就是修复后的代码逻辑,也是最符合 React 数据流直觉的写法:

JavaScript

javascript 复制代码
useEffect(() => {
  const timer = setInterval(() => {
    // ✅ 此时能读到最新的 count
    console.log('Current count:', count);
  }, 1000);

  // 每次 effect 重新执行之前 都会执行上一次的清理函数
  return () => clearInterval(timer);
}, [count]); // 👈 把 count 加入依赖数组

原理分析: 一旦把 [count] 加入依赖数组,逻辑就变了:

  1. count 变了 -> useEffect 发现依赖变了。
  2. React 先执行 cleanup 函数(clearInterval),杀掉旧的定时器。
  3. React 执行新的 useEffect,创建一个的定时器。
  4. 这个 定时器是在当前渲染闭包里创建的,所以它捕获的是最新count

潜在问题: 虽然 Bug 修好了,但带来了性能抖动 。如果 count 变化很快(比如动画),定时器会被频繁地 创建 -> 销毁 -> 创建。如果定时器间隔很短,这可能会导致计时不准。


方法二:函数式更新

如果你只是想让 count 加 1,而不关心在 setInterval 里打印日志,可以用函数式更新:

JavaScript

scss 复制代码
useEffect(() => {
  const timer = setInterval(() => {
    // ✅ prev 永远是 React 内部拿到的最新状态,不需要依赖 count
    setCount(prev => prev + 1); 
  }, 1000);
  return () => clearInterval(timer);
}, []); 

这能解决 UI 更新问题,但解决不了"在定时器里获取最新值打印"的问题。


方法三:终极大法 useRef

如果你既不想让定时器频繁重启(保持依赖为 []),又想在回调里拿到最新的值,useRef 是最佳选择。

为什么? 因为 useRef 返回的 ref 对象在组件的整个生命周期内保持引用不变 ,但它的 .current 属性是可变的。这就像一个挂在墙上的白板,无论房间(闭包)怎么换,白板还是那一块,上面的字随时能改。

JavaScript

scss 复制代码
// 1. 创建一个 ref
const countRef = useRef(count);

// 2. 每次渲染都把最新的 count 写入 ref
// 这一步确保 ref.current 永远是最新的
countRef.current = count; 

useEffect(() => {
  const timer = setInterval(() => {
    // 3. ✅ 永远读取 ref 里的最新值
    // 这里的闭包引用的是 countRef 对象本身,这个对象是永远不变的
    console.log('Current count:', countRef.current);
  }, 1000);

  return () => clearInterval(timer);
}, []); // 👈 依赖依然是空,定时器稳如泰山,不会重启!

这也是知名 Hooks 库 ahooksuseInterval 的核心实现原理。


总结

React 闭包陷阱本质上是 JavaScript 闭包机制React 声明式编程 之间的一种"沟通误会"。

  • 陷阱成因useEffectuseCallback 等 Hooks 的依赖数组写少了,导致内部函数引用了旧的渲染闭包中的变量。
  • 基础解法:补全依赖数组(但要注意副作用的频繁执行)。
  • 进阶解法 :使用 useRef 作为"逃生舱",在不重启 Effect 的情况下,透过闭包读取最新状态。
相关推荐
basestone3 小时前
🚀 从重复 CRUD 到工程化封装:我是如何设计 useTableList 统一列表逻辑的
javascript·react.js·ant design
IT=>小脑虎4 小时前
2026版 React 零基础小白进阶知识点【衔接基础·企业级实战】
前端·react.js·前端框架
IT=>小脑虎5 小时前
2026版 React 零基础小白入门知识点【基础完整版】
前端·react.js·前端框架
骑自行车的码农6 小时前
🕹️ 设计一个 React 重试
前端·算法·react.js
黎明初时8 小时前
React基础框架搭建8-axios封装与未封装,实现 API 请求管理:react+router+redux+axios+Tailwind+webpack
javascript·react.js·webpack
OEC小胖胖9 小时前
03|从 `ensureRootIsScheduled` 到 `commitRoot`:React 工作循环(WorkLoop)全景
前端·react.js·前端框架
weibkreuz9 小时前
函数柯里化@11
前端·javascript·react.js
用户9824505141810 小时前
react中useState、useEffect、useCallback、useMemo 的区别与使用场景。
react.js
chao_66666611 小时前
React Native + Expo 开发指南:编译、调试、构建全解析
javascript·react native·react.js
码丁_11711 小时前
某it培训机构前端三阶段react及新增面试题
前端·react.js·前端框架