为什么React 函数组件会产生闭包陷阱?

什么是闭包陷阱?

让我们从一道经典的题目说起。观察以下代码,判断页面上的数字如何变化:

  • A. 页面上的数字每秒增加 1
  • B. 页面上的数字始终为 0
JSX 复制代码
function Counter() {
  const [count, setCount] = useState(0);

  useEffect(() => {
    const timer = setInteral(() => {
      setCount(count + 1);  // 🚨 危险操作!
    }, 1000);
    return () => clearInterval(timer);
  }, []); 

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

正确答案是 B,数字永远定格在0!因为在 useEffect 的回调函数中,count 被捕获在闭包中,每次定时器执行时,count 都使用的是初始值 0,不会更新。

这里的解释太抽象,无法理解?没关系,要理解这个"陷阱",我们需要穿越到故事的起点------从最基础的 JavaScript 的闭包 说起。

JavaScript 中的闭包

什么是闭包?

闭包(Closure)是 JavaScript 中的一个核心特性:当一个函数内部定义了另一个函数,并且内部函数引用了外部函数的变量时,内部函数会"记住"这些变量,即使外部函数已经执行完毕。

让我们举个简单例子进行说明:

JavaScript 复制代码
function outer() {
  const x = 10;
  function inner() {
    console.log(x); // 内部函数引用了外部变量 x
  }
  return inner;
}

const innerFunc = outer();
innerFunc(); // 输出 10

在上面的这段代码中,即使 outer() 已经执行完毕,但 innerFunc(即 inner 函数)仍然能访问到 outer 中的变量 x。这就是闭包的作用。

事实上,闭包的本质是 函数与其词法环境的绑定关系,具体表现:

  • 内部函数可以访问外部函数作用域的变量
  • 外部函数执行完毕后,闭包仍保留变量引用

闭包如何"记住"变量?

JavaScript 的闭包会记住 变量在闭包创建时的值。如果变量后续被修改,闭包中引用的仍然是它"当时看到"的值。

简单来说,闭包"捕获"的是变量的引用,而不是值。

以下面 闭包"记住"旧值 为例:

JavaScript 复制代码
function createCounter() {
  let count = 0;
  return function() {
    count++;
    console.log(count);
  };
}

const counter = createCounter();
counter(); // 输出 1
counter(); // 输出 2

在上面的代码中,闭包(返回的函数)一直引用同一个 count 变量,所以每次调用 counter() 时,count 的值会被修改。

因此,我们可以这么说:闭包中的变量 count 是动态引用的,如果 count 被修改,闭包会拿到最新的值。

React 函数组件的闭包陷阱

React 函数组件的工作原理

在 React 中,一个函数组件其实就是一个JavaScript函数。因此,在函数组件中声明的函数,对于同样在函数组件中声明的变量(无论是普通变量,还是使用useState钩子声明的变量),都会产生相应的闭包,因而会产生闭包陷阱。

在继续阅读之前,让我们思考下面代码执行的真实结果:

  • A. 点击按钮时控制台始终打印0
  • B. 每次点击都会打印最新值(如1,2,3...)
JSX 复制代码
function Counter() {
  const [count, setCount] = useState(0);

  useEffect(() => {
    // 这个 effect 中的 count 是当前闭包的值
    console.log(count);
  }, []);

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

正确答案是 A。我们可以对整个流程进行分解:按钮点击-->更新count值-->组件重新渲染-->整个函数会被重新执行,生成新的闭包环境。

问题出现的原因

组件每次渲染都是独立的闭包 :每次重新渲染时,count 是一个新的变量,属于当前这次渲染的闭包。

类比生活中的例子,想象你拍了一张照片(闭包),照片中的你穿着红色衣服。后来你换了一件蓝色衣服,但照片中的你依然是红色。闭包就像这张照片,记录的是某个瞬间的状态。

结合最初代码分析

还记得文章开头的示例代码吗? 让我们重新解析:

JSX 复制代码
function Counter() {
  const [count, setCount] = useState(0);

  useEffect(() => {
    const timer = setInterval(() => {
      console.log(count); // 问题:总是输出 0
      setCount(count + 1);
    }, 1000);
    return () => clearInterval(timer);
  }, []);

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

上面的代码究竟发生了什么?

① 第一次渲染

  • count 的初始值是 0
  • useEffect 执行,启动定时器,定时器的回调函数捕获了此时的闭包,其中的 count0

② 1秒后

  • 定时器回调尝试执行 setCount(count + 1),此时 count 是闭包中的 0,所以更新后的 count 变为 1

③ 组件重新渲染

  • 新的闭包被创建,新的 count1
  • 但旧的定时器回调仍然引用旧的闭包(count0),所以下一次执行时,仍然会执行 setCount(0 + 1),导致 count 永远卡在 1

总结

在 React 函数组件中,当满足下面条件时,即会形成闭包陷阱:

  1. 持久化引用:在闭包中创建长期存活的引用(如定时器、事件监听)
  2. 跨渲染周期:后续操作访问旧闭包中的状态值
  3. 依赖数组未正确声明 :未在依赖数组(如 useEffect 的第二个参数)中声明所有引用的变量,从而导致副作用函数持续引用旧闭包中的值。

但是,闭包机制当然不是一无是处,它是React函数组件的核心优势,有着天然的状态隔离性、明确的依赖关系。

相关推荐
G_whang1 小时前
jenkins自动化部署前端vue+docker项目
前端·自动化·jenkins
凌辰揽月3 小时前
AJAX 学习
java·前端·javascript·学习·ajax·okhttp
然我4 小时前
防抖与节流:如何让频繁触发的函数 “慢下来”?
前端·javascript·html
鱼樱前端4 小时前
2025前端人一文看懂 Broadcast Channel API 通信指南
前端·vue.js
烛阴5 小时前
非空断言完全指南:解锁TypeScript/JavaScript的安全导航黑科技
前端·javascript
鱼樱前端5 小时前
2025前端人一文看懂 window.postMessage 通信
前端·vue.js
快乐点吧5 小时前
【前端】异步任务风控验证与轮询机制技术方案(通用笔记版)
前端·笔记
pe7er6 小时前
nuxtjs+git submodule的微前端有没有搞头
前端·设计模式·前端框架
七月的冰红茶6 小时前
【threejs】第一人称视角之八叉树碰撞检测
前端·threejs
爱掉发的小李6 小时前
前端开发中的输出问题
开发语言·前端·javascript