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

相关推荐
Nueuis14 分钟前
微信小程序前端面经
前端·微信小程序·小程序
_r0bin_3 小时前
前端面试准备-7
开发语言·前端·javascript·fetch·跨域·class
IT瘾君3 小时前
JavaWeb:前端工程化-Vue
前端·javascript·vue.js
potender3 小时前
前端框架Vue
前端·vue.js·前端框架
站在风口的猪11083 小时前
《前端面试题:CSS预处理器(Sass、Less等)》
前端·css·html·less·css3·sass·html5
程序员的世界你不懂4 小时前
(9)-Fiddler抓包-Fiddler如何设置捕获Https会话
前端·https·fiddler
MoFe14 小时前
【.net core】天地图坐标转换为高德地图坐标(WGS84 坐标转 GCJ02 坐标)
java·前端·.netcore
去旅行、在路上4 小时前
chrome使用手机调试触屏web
前端·chrome
Aphasia3115 小时前
模式验证库——zod
前端·react.js
lexiangqicheng5 小时前
es6+和css3新增的特性有哪些
前端·es6·css3