一文搞懂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可以在组件中持久性保存数据。
相关推荐
jin12332223 分钟前
基于React Native鸿蒙跨平台地址管理是许多电商、外卖、物流等应用的重要功能模块,实现了地址的添加、编辑、删除和设置默认等功能
javascript·react native·react.js·ecmascript·harmonyos
2501_9209317042 分钟前
React Native鸿蒙跨平台医疗健康类的血压记录,包括收缩压、舒张压、心率、日期、时间、备注和状态
javascript·react native·react.js·ecmascript·harmonyos
落霞的思绪1 小时前
配置React和React-dom为CDN引入
前端·react.js·前端框架
Hacker_Z&Q1 小时前
CSS 笔记2 (属性)
前端·css·笔记
Anastasiozzzz1 小时前
LeetCode Hot100 295. 数据流的中位数 MedianFinder
java·服务器·前端
橙露2 小时前
React Hooks 深度解析:从基础使用到自定义 Hooks 的封装技巧
javascript·react.js·ecmascript
Exquisite.2 小时前
Nginx
服务器·前端·nginx
2501_920931702 小时前
React Native鸿蒙跨平台使用useState管理健康记录和过滤状态,支持多种健康数据类型(血压、体重等)并实现按类型过滤功能
javascript·react native·react.js·ecmascript·harmonyos
打小就很皮...2 小时前
dnd-kit 实现表格拖拽排序
前端·react.js·表格拖拽·dnd-kit
Ulyanov2 小时前
从静态到沉浸:打造惊艳的Web技术发展历程3D时间轴
前端·javascript·html5·gui开发