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 函数式组件中存在的闭包过期问题(由浅入深理解闭包与闭包过期的产生) - 掘金

相关推荐
UI设计和前端开发从业者1 分钟前
UI前端大数据处理策略优化:基于云计算的数据存储与计算
前端·ui·云计算
前端小巷子28 分钟前
Web开发中的文件上传
前端·javascript·面试
翻滚吧键盘1 小时前
{{ }}和v-on:click
前端·vue.js
上单带刀不带妹1 小时前
手写 Vue 中虚拟 DOM 到真实 DOM 的完整过程
开发语言·前端·javascript·vue.js·前端框架
前端风云志1 小时前
typescript结构化类型应用两例
javascript
杨进军2 小时前
React 创建根节点 createRoot
前端·react.js·前端框架
ModyQyW2 小时前
用 AI 驱动 wot-design-uni 开发小程序
前端·uni-app
说码解字2 小时前
Kotlin lazy 委托的底层实现原理
前端
gnip2 小时前
总结一期正则表达式
javascript·正则表达式
爱分享的程序员3 小时前
前端面试专栏-算法篇:18. 查找算法(二分查找、哈希查找)
前端·javascript·node.js