一文搞懂React Hooks闭包问题

hooks为什么会出现闭包

在使用React Hooks时可能会遇到一个比较常见的问题,就是React Hooks的闭包陷阱,这与React的更新机制有关,为了搞懂React Hooks出现的闭包陷阱,先了解下什么闭包。

闭包

闭包(closure)简单的来说就是定义在一个函数内部的函数,闭包可以让函数内部的变量保存在内存中,从而延长一个变量的生命周期。因为js特殊的变量作用域,一些高级的特性中都要靠闭包来实现。

代码举例

ts 复制代码
const App: React.FC = () => {
  const [count, setCount] = React.useState(0);

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

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

export default App;

App组件初次渲染执行useEffect中的定时器,每秒执行一次,将count值加1。运行代码后发现,count的值只变化了一次,而且打印的count值都是0。

这就是React闭包陷阱的一个典型场景。

首先,在首次执行定时器时,count值是0,setCount(count+1)页面重新渲染,界面显示1。

然后在函数组件重新(每次)渲染时,都会重新执行函数体中的代码。但是在重新执行的过程中,count变量在闭包函数中,在首次渲染时捕获到函数内部的count值。而且在后续的渲染中即使count的值发生了变化,但之前捕获的变量值仍然保持着原来的值,就形成了闭包陷阱。

原因

根据上面的例子,我们可以给useEffect加上count依赖,这样界面就是我们期望的渲染逻辑,但是加上count依赖后,每次都会重新销毁和创建定时器,这就会导致性能损耗以及定时器中的逻辑并不是1s执行一次。useState暴露出来的setState函数还可以传递一个函数,函数的参数就是最新的state值。返回数据会作为setState的参数。

界面渲染正常,但是在定时器的代码中获取到的count值不对,还都是0。这是因为打印的count值还是从闭包中拿的,但是渲染的count值通过回调函数拿的是最新的。

而且上面说的解法依赖于useState提供的回调函数,但是hooks闭包陷阱不仅仅只在使用useState时会出现。闭包陷阱的出现是由于函数组件的更新机制产生的。

js 复制代码
import React, { useEffect } from "react";

const Child = ({ onClick }: {onClick: () => void}) => {
  console.log("render child......");
  return (
    <button onClick={onClick} >
      Button
    </button>
  );
};

const App: React.FC = () => {
  const [count, setCount] = React.useState(0);

  useEffect(() => {
    const time = setInterval(() => {
      setCount((count) => count + 1);
    }, 1000);
    return () => {
      clearInterval(time);
    };
  }, []);

  const handleClick = () => {
    console.log("count:", count);
  };

  return (
    <>
      <h2>{count}</h2>
      <Child onClick={handleClick} />
    </>
  );
};

export default App;

在上面这个例子中没有闭包陷阱,逻辑也都正确,但是有一个优化问题,每次重新渲染组件的时候handleClick都会被重新定义,从而导致Child组件重新渲染。我们使用useCallback包裹,然后用memo包裹Child组件优化一下。

每次count变化时,重新渲染App组件,Child组件不会重新渲染了,但是点击button,打印出来的count值永远都是0,这里就出现了React Hooks的闭包陷阱。

如何解决闭包陷阱

使用useRef。

使用useRef保存数据,然后每次需要最新的数据时在useRef的current中拿。

tsx 复制代码
import React, { memo, useCallback, useEffect, useLayoutEffect, useRef } from "react";

const Child = memo(({ onClick }: {onClick: () => void}) => {
  console.log("render child");
  return (
    <button onClick={onClick} >
      Button
    </button>
  );
});

const App: React.FC = () => {
  const [count, setCount] = React.useState(0);

  const getCountInfo = useCallback(() => {
    console.log("count:", count);
  },[count])
  const ref = useRef(getCountInfo);

  useEffect(() => {
    const time = setInterval(() => {
      setCount((count) => count + 1);
    }, 1000);
    return () => {
      clearInterval(time);
    };
  }, []);

  useLayoutEffect(() => {
    ref.current = getCountInfo
  },[getCountInfo])

  const handleClick = useCallback(() => {
    ref.current()
  }, [])
  

  return (
    <>
      <h2>{count}</h2>
      <Child onClick={handleClick} />
    </>
  );
};

export default App;
  • useEffect没有依赖只执行一次。
  • 定时器每次的更新的count都是最新的。
  • useCallback和memo做了优化,避免了不必要的渲染。
  • useRef保存了最新的getCountInfo函数,每次调用函数获取的都是最新的count数据。

总结

  • 闭包陷阱的产生并不是hooks本身的问题,是因为在React的更新机制的基础上,使用hooks时,我们对变量的引用和更新处理不当时,就会容易出现闭包陷阱导致出现的数据不正确的问题。
  • 一般的原因形成就是hooks函数中使用了state,但是因为各种原因没有在依赖数组中加上state依赖,导致hooks函数的回调中使用的state还是之前的。
  • 使用useRef来解决闭包陷阱,因为useRef可以在组件中持久性保存数据。
相关推荐
崔庆才丨静觅2 小时前
hCaptcha 验证码图像识别 API 对接教程
前端
passerby60613 小时前
完成前端时间处理的另一块版图
前端·github·web components
掘了3 小时前
「2025 年终总结」在所有失去的人中,我最怀念我自己
前端·后端·年终总结
崔庆才丨静觅3 小时前
实用免费的 Short URL 短链接 API 对接说明
前端
崔庆才丨静觅3 小时前
5分钟快速搭建 AI 平台并用它赚钱!
前端
崔庆才丨静觅3 小时前
比官方便宜一半以上!Midjourney API 申请及使用
前端
Moment4 小时前
富文本编辑器在 AI 时代为什么这么受欢迎
前端·javascript·后端
崔庆才丨静觅4 小时前
刷屏全网的“nano-banana”API接入指南!0.1元/张量产高清创意图,开发者必藏
前端
剪刀石头布啊4 小时前
jwt介绍
前端
爱敲代码的小鱼4 小时前
AJAX(异步交互的技术来实现从服务端中获取数据):
前端·javascript·ajax