为什么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函数组件的核心优势,有着天然的状态隔离性、明确的依赖关系。

相关推荐
DevSecOps选型指南20 分钟前
SBOM情报预警 | 恶意NPM组件窃取Solana智能合约私钥
前端·npm·智能合约·软件供应链安全厂商·供应链安全情报
boring_student31 分钟前
CUL-CHMLFRP启动器 windows图形化客户端
前端·人工智能·python·5g·django·自动驾驶·restful
SailingCoder37 分钟前
递归陷阱:如何优雅地等待 props.parentRoute?
前端·javascript·面试
关山月1 小时前
React 中的 SSR 深度探讨
前端
yzhSWJ2 小时前
vue设置自定义logo跟标题
前端·javascript·vue.js
vvilkim2 小时前
Vue.js 中的 Tree Shaking:优化你的应用性能
前端·javascript·vue.js
杨超越luckly2 小时前
HTML应用指南:利用GET请求获取猫眼电影日票房信息——以哪吒2为例
前端·数据挖掘·数据分析·html·猫眼
狼性书生3 小时前
uniapp 实现的下拉菜单组件
前端·uni-app·vue·组件·插件
浪裡遊3 小时前
uniapp中的vue组件与组件使用差异
前端·vue.js·uni-app
风无雨3 小时前
react 中 key 的使用
前端·react.js·前端框架