react学习3: 闭包陷阱

看这样一个组件,通过定时器不断的累加 count:

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

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

    useEffect(() => {
        setInterval(() => {
            console.log(count);
            setCount(count + 1);
        }, 1000);
    }, []);

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

export default App;

你觉得这个 count 会每秒加 1 么? 不会。

可以看到,setCount 时拿到的 count 一直是 0。

为什么呢?

大家可能觉得,每次渲染都引用最新的 count,然后加 1,所以觉得没问题:

但是,现在 useEffect 的依赖数组是 [],也就是只会执行并保留第一次的 function。

而第一次的 function 引用了第一次渲染的 count,当第二次渲染的时候,取的count还是第一次function里面的count,所以形成了闭包,使用了自由变量(不是在自己函数内部定义的变量)就是闭包。

也就是实际上的执行是这样的:

这就导致了每次执行定时器的时候,都是在 count = 0 的基础上加一。

这就叫做 hook 的闭包陷阱。

那怎么解决这个问题呢?不让它形成闭包不就行了?

这时候可以用 setState 的另一种参数:

js 复制代码
useEffect(() => {
        setInterval(() => {
            console.log(count);
            setCount(count => count + 1);
        }, 1000);
    }, []);

这次并没有形成闭包,每次的 count 都是参数传入的上一次的 state。

但有的时候,是必须要用到 state 的,也就是肯定会形成闭包,

比如这里,console.log 的 count 就用到了外面的 count,形成了闭包,但又不能把它挪到 setState 里去写:

js 复制代码
useEffect(() => {
    setInterval(() => {
        console.log(count);
        setCount(count => count + 1);
    }, 1000);
}, []);

这种情况怎么办呢?

还记得 useEffect 的依赖数组是干啥的么?当依赖变动的时候,会重新执行 effect。

所以可以这样:

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

function App() {

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

    useEffect(() => {
        console.log(count);

        const timer = setInterval(() => {
            setCount(count + 1);
        }, 1000);

        return () => {
            clearInterval(timer);
        }
    }, [count]);

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

export default App;

依赖数组加上了 count,这样 count 变化的时候重新执行 effect,那执行的函数引用的就是最新的 count 值。

但是,有定时器不能重新跑 effect 函数,那怎么做呢?

可以用 useRef

js 复制代码
import { useEffect, useState, useRef, useLayoutEffect } from 'react';

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

    const updateCount = () => {
        setCount(count + 1);
    };
    const ref = useRef(updateCount);

    ref.current = updateCount;

    useEffect(() => {
        const timer = setInterval(() => ref.current(), 1000);

        return () => {
            clearInterval(timer);
        }
    }, []);

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

export default App;

通过 useRef 创建 ref 对象,保存执行的函数,每次渲染更新 ref.current 的值为最新函数。

这样,定时器执行的函数里就始终引用的是最新的 count。

useEffect 只跑一次,保证 setIntervel 不会重置,是每秒执行一次。

执行的函数是从 ref.current 取的,这个函数每次渲染都会更新,引用着最新的 count。

讲 useRef 的时候说过,ref.current 的值改了不会触发重新渲染,

它就很适合这种保存渲染过程中的一些数据的场景。

再来看一个例子:

js 复制代码
import { type FC, useState, useRef } from 'react'

const Demo: FC = () => {
  const [count, setCount] = useState(0)

  const add = () => {
    setCount(count + 1)
  }

  const alertFn = () => {
    setTimeout(() => {
      alert(count)
    }, 3000)
  }

  return (
    <div>
      <p>闭包陷阱</p>
      <div>
        <p>{count}</p>
        <button onClick={add}>add</button>
        <button onClick={alertFn}>alert</button>
      </div>
    </div>
  )
}

export default Demo

当你先点击alertFn函数,然后快速点击add函数,此时 alert(count) 中的仍然是初始值0,这也是因为闭包导致的。

怎么做呢?仍然是用useRef来解决:

js 复制代码
import { type FC, useState, useRef } from 'react'

const Demo: FC = () => {
  const [count, setCount] = useState(0)

  const countRef = useRef(0)
  console.log('count改变时都会执行', count)
  countRef.current = count

  const add = () => {
    setCount(count + 1)
  }

  const alertFn = () => {
    setTimeout(() => {
      // alert(count)
      alert(countRef.current)
    }, 3000)
  }

  return (
    <div>
      <p>闭包陷阱</p>
      <div>
        <p>{count}</p>
        <button onClick={add}>add</button>
        <button onClick={alertFn}>alert</button>
      </div>
    </div>
  )
}

export default Demo

总结

闭包陷阱指的是在 React 组件中,由于闭包特性捕获了当前作用域的 state/prop ,导致在后续操作(如异步函数、定时器)中访问到的仍是旧值,而非最新状态。

原因:

  1. React 函数组件的执行机制

函数组件每次渲染时,都会生成一个全新的函数作用域,其中的 state、prop 都是该次渲染的 "快照"。

React 的 state 是不可变的,setState(或 setXxx)并不会修改当前 state,而是触发新的渲染并生成新的 state。

  1. 闭包对作用域的捕获

当在组件中定义回调函数(如事件处理、定时器、异步操作)时,函数会捕获当前渲染作用域中的 state/prop。即使后续状态更新触发了新的渲染,旧的回调函数仍持有对旧作用域中变量的引用。

闭包一般会导致内存泄漏,但是react中异步函数访问旧的 state 本身不会直接导致内存泄漏。这是因为异步函数执行完毕后,若没有其他引用指向这个闭包,它会被垃圾回收机制清理。旧的 state 本身是不可变的快照,即使被闭包引用,只要不再被使用,最终也会被回收。

相关推荐
该用户已不存在6 小时前
Vibe Coding 入门指南:从想法到产品的完整路径
前端·人工智能·后端
Pedro6 小时前
Flutter - 日志不再裸奔:pd_log 让打印有型、写盘有序
前端·flutter
申阳6 小时前
Day 3:01. 基于Nuxt开发个人呢博客项目-初始化项目
前端·后端·程序员
三小河6 小时前
解决 React + SSE 流式输出卡顿:Nginx 关键配置实战
前端·架构·前端框架
玖月晴空6 小时前
Uniapp 速查文档
前端·微信小程序·uni-app
琉-璃6 小时前
vue3+ts 任意组件间的通信 mitt的使用
前端·javascript·vue.js
FogLetter6 小时前
React Fiber 机制:让渲染变得“有礼貌”的魔法
前端·react.js
不想说话的麋鹿6 小时前
「项目前言」从配置程序员到动手造轮子:我用Vue3+NestJS复刻低代码平台的初衷
前端·程序员·全栈
JunpengHu7 小时前
esri-leaflet介绍
前端