一文搞懂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可以在组件中持久性保存数据。
相关推荐
zqx_737 分钟前
随记 前端框架React的初步认识
前端·react.js·前端框架
惜.己1 小时前
javaScript基础(8个案例+代码+效果图)
开发语言·前端·javascript·vscode·css3·html5
什么鬼昵称1 小时前
Pikachu-csrf-CSRF(get)
前端·csrf
长天一色2 小时前
【ECMAScript 从入门到进阶教程】第三部分:高级主题(高级函数与范式,元编程,正则表达式,性能优化)
服务器·开发语言·前端·javascript·性能优化·ecmascript
NiNg_1_2342 小时前
npm、yarn、pnpm之间的区别
前端·npm·node.js
秋殇与星河2 小时前
CSS总结
前端·css
NiNg_1_2342 小时前
Vue3 Pinia持久化存储
开发语言·javascript·ecmascript
读心悦2 小时前
如何在 Axios 中封装事件中心EventEmitter
javascript·http
BigYe程普2 小时前
我开发了一个出海全栈SaaS工具,还写了一套全栈开发教程
开发语言·前端·chrome·chatgpt·reactjs·个人开发
神之王楠2 小时前
如何通过js加载css和html
javascript·css·html