Hook 闭包陷阱

我们在努力扩大自己,以靠近,以触及我们自身以外的世界。 ---博尔赫斯谈话录-

闭包陷阱

React Hooks 中的闭包陷阱主要会发生在两种情况:

  • 在 useState 中使用闭包;
  • 在 useEffect 中使用闭包。

useState

例子一

闭包陷阱的原因是因为在闭包环境下使用了 Hook,实际上这是 JavaScript 的闭包问题的延伸,也就是说,在一些可能保留旧状态的引用的情况下,使用了 Hook 函数,举个例子🙋‍♀️🌰:

js 复制代码
function Counter() {
  const [count, setCount] = useState(0);
  const buttonRef = useRef(null);

  const handleClick = () => {
    setCount(count + 1); //这里无论执行多少次,都是setCount( 0 + 1),因为页面还没有渲染,需要 1s 之后渲染,将 count 的值改变之后,才会+1
    // 为什么setCount(count=>count+1); 就没问题,因为它每次是一个回调函数,将 count+1 了
  };

  useEffect(() => {
    const buttonDom = buttonRef.current;
    if (buttonDom) {
      buttonDom.addEventListener('click', handleClick);
    }
    return () => {
      if (buttonDom) {
        buttonDom.removeEventListener('click', handleClick);
      }
    };
  }, []);
  return (
    <div>
      {count}
      <button ref={buttonRef}>count++</button>
    </div>
  );
}

在上面的代码中,这里只有第一次点击时count变成了1,之后不再改变。

原因是useEffectHook 的依赖是空数组,只会在第一次的时候执行,所以保留的是最初的 count 值,也就是 count = 0,useEffect 中执行了 buttonDom.addEventListener('click', handleClick);,就是为buttonDom添加了函数const handleClick = () => {setCount(count + 1); };,此时,handleClick函数保留了count = 0,

后续 button 点击执行,由于useEffect 的依赖项是空数组,所以buttonDom.addEventListener('click', handleClick);这句话不再执行,所以 button 绑定的点击事件handleClick中的 count 值也不再更新,保存的变量 count 一直是count = 0的状态,所有点击之后+1 变成了 1,再次点击还是由 0 加一变成 1,视图不再更新。

例子二

js 复制代码
function Counter() {
  
  const [count, setCount] = useState(0);
  
  const handleClick = () => {
    setTimeout(() => {
      setCount(count + 1);
    }, 1000);
  };
  
  const handleReset = () => {
    setCount(0);
  };
  
  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={handleClick}>Increment</button>
      <button onClick={handleReset}>Reset</button>
    </div>
  );
}

这里的代码,首先定义了一个 handleClick 函数,他使用了一个闭包来缓存 count 值,每次点击触发 handleClick 函数,1s 后 setCount 会将 count 值加 1,在这 1s 内,无论我们点击多少次 Increment 按钮,count 值都只会加 1。

原因是: setCount(count + 1);中的count 是闭包缓存的值,后续Counter 组件在 count 的值的改变就不会影响到闭包里面的值变化,这个值是始终不变的。1s 后 setTimeout 中的 setCount 生效,函数式组件 Counter 重新执行,会生成新的 handleClick 方法,形成新的闭包,此时闭包中缓存的 count 值也就变成了最新的 count 值。再继续点击 Increment 按钮,又会重复上述循环,每过 1s count 值会加 1.

结论:

在React中,useState hook返回的更新state的函数,即setCount函数,可以接受一个回调函数作为参数。这个回调函数会接受当前state的值作为参数,然后返回一个新的state值。React会使用这个新的state值来更新组件的状态。

在上述代码中,通过使用回调函数的形式来更新count的值,这个回调函数会接受 currentCount 作为参数,即当前的count值,而不是从外部直接引用count变量。这样,即使在闭包中使用了count变量,也不会受到影响,因为回调函数内部的 currentCount 变量是函数作用域内的局部变量,不会受到外部变量的影响。这种方式可以避免闭包陷阱,保证组件可以正确更新状态。

解决方案

方案一:添加依赖项(不推荐👎)

我们可以将要改变的状态作为依赖添加到依赖项中:

js 复制代码
useEffect(() => {
  const buttonDom = buttonRef.current;
  if (buttonDom) {
    buttonDom.addEventListener('click', handleClick);
  }
  return () => {
    if (buttonDom) {
      buttonDom.removeEventListener('click', handleClick);
    }
  };
}, [count]);

这种方式为什么不推荐呢?

  • 因为当一个组件复杂时,依赖项的增加会导致Hook中逻辑的复杂性
  • 此外,这种方式生效的原因是因为我们触发了setCount导致值变化了,useEffect再次执行,实际上是创建了一个新的函数,新的词法环境,我们还需要注意其中副作用的清理。

方案二:回调形式的setState

React 为我们提供的setState可以使用回调形式,这样我们总能拿到上一个值,然后在此基础上进行修改:

js 复制代码
const handleClick = () => {
  setCount(preCount=>preCount+1); 
};

这种方式生效的原因是尽管我们词法环境并没有发生变化,但是我们每次触发setCount时使用的不再是词法环境中保存的那个count了,而是React获取了上一次的状态。

方案三:使用Ref同步状态

js 复制代码
const [count, setCount] = useState(0);
const countRef = useRef(count);
const buttonRef = useRef(null);
const handleClick = () => {
  countRef.current += 1;
  setCount(countRef.current);
};

这种方式生效的原因是,我们调用setCount时,使用的都是ref中存储的值,而不是当前词法环境中的count

你发现了吗:方案二 和 方案三 十分相似,都是避免使用词法环境中的count,将状态保存在其他地方进行修改和使用,setState方案相当于React帮助我们保存了一份count,而Ref相当于我们自己保存了一份

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

function Dong() {



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

  const val = useRef(count);
  //必须写这句话,否则么有作用,因为 val 会一直是 0
  // 写了这句话,组建再次渲染的时候会 val 重新赋值
  val.current = count;

  console.log('val+' + val.current);


  useEffect(() => {
    setInterval(() => {
      setCount(count => count + 1);
    }, 500);
  }, []);

  useEffect(() => {
    setInterval(() => {
      console.log(val.current);
    }, 500);
  }, []);

  return <div>guang</div>;
}

export default Dong;

useEffect闭包陷阱

js 复制代码
function App() {
  const [count, setCount] = useState(0);
  
  useEffect(() => {
    const timer = setInterval(() => {
      console.log(count);
    }, 1000);
    return () => clearInterval(timer);
  }, []);
  
  const handleClick = () => {
    setCount(count + 1);
  };
  
  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={handleClick}>Increment</button>
    </div>
  );
}

这里就比较简单了,由于 useEffect 只会在组件首次渲染时执行一次,因此闭包中的 count 变量始终是首次渲染时的变量,而不是最新的值。

人人都要理解的-react闭包陷阱 - 掘金

解决办法

为了避免这种闭包陷阱,可以使用 useEffect Hook 来更新状态。例如,以下代码中,通过 useEffect Hook 来更新 count 的值,就可以避免闭包陷阱:

js 复制代码
useEffect(() => {
  const timer = setInterval(() => {
    console.log(count);
  }, 1000);
  return () => clearInterval(timer);
}, [count]);

引用

ahooks源码系列(一):React 闭包陷阱 - 掘金

React 函数式组件中存在的闭包过期问题(由浅入深理解闭包与闭包过期的产生) - 掘金

相关推荐
Martin -Tang30 分钟前
vite和webpack的区别
前端·webpack·node.js·vite
迷途小码农零零发31 分钟前
解锁微前端的优秀库
前端
王解1 小时前
webpack loader全解析,从入门到精通(10)
前端·webpack·node.js
老码沉思录1 小时前
写给初学者的React Native 全栈开发实战班
javascript·react native·react.js
老码沉思录1 小时前
React Native 全栈开发实战班 - 第四部分:用户界面进阶之动画效果实现
react native·react.js·ui
我不当帕鲁谁当帕鲁1 小时前
arcgis for js实现FeatureLayer图层弹窗展示所有field字段
前端·javascript·arcgis
那一抹阳光多灿烂2 小时前
工程化实战内功修炼测试题
前端·javascript
放逐者-保持本心,方可放逐2 小时前
微信小程序=》基础=》常见问题=》性能总结
前端·微信小程序·小程序·前端框架
毋若成4 小时前
前端三大组件之CSS,三大选择器,游戏网页仿写
前端·css
红中马喽5 小时前
JS学习日记(webAPI—DOM)
开发语言·前端·javascript·笔记·vscode·学习