React闭包陷阱

闭包

外部访问函数内部变量,导致内部变量不被释放,一直保留

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

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

hooks闭包陷阱

是什么

在 React 函数组件中,每一次 重新渲染(re-render) 都会:

  • 重新执行整个函数组件;
  • 创建新的变量、函数、作用域;
  • 但 旧的闭包(closure)仍然保留旧的状态值。

⚠️ 当在异步回调、事件回调或定时器中引用旧的状态值时,就会掉进闭包陷阱!

举个🌰

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

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

  useEffect(() => {
    const timer = setInterval(() => {
      console.log('Current count:', count);
      setCount(count + 1); // ⚠️ 闭包陷阱:count 永远是初始值 0
    }, 1000);
    return () => clearInterval(timer);
  }, []); // 空依赖数组,只绑定初始 count
  // ⛔ useEffect 内部的函数引用了旧的 count

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

这个时候每次打印的count均为0 展示的count值保持不变

直接赋值

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

function Counter() {
  const [id, setId] = useState(1);
  const [data, setData] = useState([]);

  const fetchData = id => {
    return new Promise(resolve => {
      setTimeout(() => {
        resolve({ id, title: `Title ${id}`, content: `Content ${id}` });
      }, 2000);
    });
  };

  useEffect(() => {
    fetchData(id).then(newData => {
      setData(data.concat(newData));
    });
  }, [id]);


  return (
    <>
      <ul>
        {data.map(item => (
          <li key={item.id}>data is {item.title}</li>
        ))}
      </ul>
      <button onClick={() => setId(id + 1)}>set ID </button>
    </>
  );
}

使用函数式

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

function Counter() {
  const [id, setId] = useState(1);
  const [data, setData] = useState([]);

  const fetchData = id => {
    return new Promise(resolve => {
      setTimeout(() => {
        resolve({ id, title: `Title ${id}`, content: `Content ${id}` });
      }, 2000);
    });
  };

  useEffect(() => {
    fetchData(id).then(newData => {
      setData((prevData) => [...prevData, newData]);
    });
  }, [id]);


  return (
    <>
      <ul>
        {data.map(item => (
          <li key={item.id}>data is {item.title}</li>
        ))}
      </ul>
      <button onClick={() => setId(id + 1)}>set ID </button>
    </>
  );
}

为什么

React 函数组件不是持续运行的函数,而是 每次渲染都会重新执行。 所以每次渲染时:

  • count是当前那次渲染的值;

  • useEffect 回调引用的变量,取决于它创建时的那次渲染环境;

  • 如果依赖数组为空 \[\],那 effect 只运行一次 → 永远绑定初始闭包。

对于上述例子来讲

javascript 复制代码
setCount(count + 1);

这行代码不是"立即执行 +1",而是:

  • 把当前渲染时的 count 值取出来;
  • 计算 count + 1;
  • 通知 React "请在下一次渲染时,把状态设置为这个新值"。

也就是说,这句代码依赖的是当前闭包里的 count。相当于一直在执行setCount(1);所以渲染的count不会发生变化。

怎么做

函数式更新

上述情况虽然打印的一直是0,但是实际渲染的count发生了变化

javascript 复制代码
import React, { useState, useEffect } from 'react';
function Counter() {
  const [count, setCount] = useState(0);

  useEffect(() => {
    const timer = setInterval(() => {
      console.log('Current count:', count);
       setCount(prev => prev + 1); // ✅ 使用最新状态
    }, 1000);
    return () => clearInterval(timer);
  }, []); 

  return <h2>{count}</h2>;
}
为什么这种方法可以正常渲染count

React中setState有两种写法

  • 直接赋值

    javascript 复制代码
    setCount(count + 1);
  • 函数式更新

    javascript 复制代码
    setCount(prev => prev + 1);

这两种的差异在于执行时机

  • 直接赋值

    • 表达式调用时立即执行
    • 立即计算出结果(0 + 1 = 1)并把结果交给React
    • 在闭包中count一直为0 那么用于是setCount(1)
  • 函数式更新

    • 不会立即执行

    • React将函数存储起来,稍后执行时传入最新的state,不依赖闭包中的值,每次都拿最新的

    • 个人理解为 函数式取值跳出了闭包

增加依赖

打印的count和渲染的count均会发生变化,但会导致频繁清除重建effect

javascript 复制代码
useEffect(() => {
  const timer = setInterval(() => {
    console.log('Current count:', count);
    setCount(count + 1);
  }, 1000);

  return () => clearInterval(timer);
}, [count]); // ✅ 每次 count 改变时重新注册 effect

useRef

javascript 复制代码
const [count, setCount] = useState(0);
const countRef = React.useRef(count);

useEffect(() => {
  countRef.current = count; // 更新 ref
}, [count]);

useEffect(() => {
  const timer = setInterval(() => {
    console.log('Current count:', countRef.current); // ✅ 读最新值
  }, 1000);
  return () => clearInterval(timer);
}, []);
相关推荐
小码哥_常3 小时前
一套自己部署的 IM,客户端是同一份 Flutter
前端·后端
蜗牛攻城狮6 小时前
微前端四大方案选型对比与最佳实践:qiankun、micro-app、wujie、Module Federation
前端·微前端·qiankun·前端架构·micro-app·wujie
她说可以呀7 小时前
Vue插槽
前端·javascript·vue.js
qq_448011167 小时前
Vue + Jinja2两套模版与法冲突造成undefinedError
前端·javascript·vue.js
aixingpan8 小时前
aixingpan.cn API开发文档:前端查询夏令时和时区接口指南
前端·php
2601_958352908 小时前
23.5×19mm 换掉一整条音频链?|A-68 集成了什么:ADC / DAC / 24.576MHz 时钟 / AEC / 波束降噪,主控仅需 6 根线
前端·音视频·ai降噪·回声消除·语音模块
志尊宝9 小时前
AI原生前端开发(039):AI辅助的Nuxt 4全栈开发
前端·vue.js·ai-native
FRing0310 小时前
JavaScript入门到精通三十二(深拷贝、浅拷贝、递归函数)
开发语言·前端·javascript
excel10 小时前
双 Token 与前端使用 Crypto 公私钥鉴权换 Token,哪种方案更好?
前端·后端
外收内放10 小时前
Python基础语法练习题(73)
java·前端·python