浅谈React hooks函数组件的闭包陷阱以及解决方法

相信使用React hooks开发的小伙伴肯定或多或少都遇到过一些“奇怪”的“闭包陷阱”的场景(其实是符合逻辑的)。如下:

const testDemo = () => {
    const [count, setCount] = useState(1)
    useEffect(() => {
        setInterval(() => {
           console.log(count)
        }, 1000)
        //闭包陷阱
    }, [])
    const handleClick = () => {
        setCount(count+1)
    }

    return (
        <div onClick={handleClick}>
            click to add, count: {count}
        </div>
    )
    
}

我们期待在点击之后, 打印出count也能更新, 但是事实是, 每次打印出的count都没变.

什么是闭包陷阱:

简单来说, 就是react hooks在渲染的时候维护了一个链表, 来记录useState和useEffect的位置和值, (这也是state不能使用if else的原因, 因为可能会导致链表中state useEffect的顺序错乱, 从而不能获取到正确的数值)

在每次state更新时, 链表从头开始重新渲染, 但是由于上面示例中useEffect没有依赖任何state, 所以只有在第一次渲染的时候才会触发, setCount渲染更新时, useEffect里面的回调函数并没有触发 因此里面的setInterval里面的count还是初始化时的值,
并没有获取到最新的. 这就是闭包陷阱
 

原因:

在函数组件中,如果我们在回调函数中使用了 useState 创建的值,那么闭包就会产生。闭包在函数组件创建时产生,他会缓存创建时的 state 的值。

在hook里面的函数,如果是useEffect(()=>(),[])这种写法,即只组件挂载阶段执行,那么在这里面的函数,拿到的值始终都只是初始化时候的值,就算你在其他地方修改了值之后,也是获取不到最新值的。

解决方案:

1) 最简单的:useRef

因为useRef 每次拿到的都是这个对象本身, 是同一个内存空间的数据, 所以可以获取到最新的值。

同理, 我们如果这样浅拷贝, 也是可以获取到最新的值的(比如Object.assign方法)

原理:使用对象的引用, 直接获取对象本身的数据

2)清除-重建:使用useEffect在页面更新时清除产生的闭包

由于在组件其他更新的时候,总是会走 useEffect 这个函数,处于更新模式情况下,可以就是采取 清除-重建 的方式进行

const [count, setCount] = useState(0)
    let myInterval = null

    const interval = () => {
        myInterval = setInterval(() => {
            console.log(count)
            if (count > 5) {
                clearInterval(myInterval)
            }
            setCount(count + 1)
        }, 1000)
    }
    useEffect(() => {
        //由于更新时清除了, 所以要重新模拟一下点击时的操作, 确保继续运行
        if (count > 0) {
            interval()
        }
        //更新时清除掉interval
        return () => clearInterval(myInterval)
    })
    return <div onClick={interval}>click count add : {count}</div>

可以不设置参数,那么useEffect每次更新都会调用,也可以设置useEffect的第二个参数依赖项,这个是一个数组类型的参数,将依赖项传入,依赖项的值发生改变后,会重新执行useEffect,拿到最新的值。
如果里面写了定时器,最好return出去一个函数里清除定时器。如果是addEventListener监听事件,那么就return一个函数清除监听事件。

3)如果仅仅是变量的计算操作,那么更新时使用useState的函数写法

// 异常的写法
import { useState } from 'react';
 
export default function Counter() {
  const [count, setCount] = useState(0)
 
  function handle() {
    setCount(count + 1)
    // 当 setTimeout 执行时,
    // 注意:回调函数的 count 值不是 1,而是 0
    setTimeout(() => {
      setCount(count + 2)
    }, 0)
  }
 
  return (
    <div>
      <div>{count}</div>
      <button onClick={handle}>递增</button>
    </div>
  )
}


// 正常的写法
const [count, setCount] = useState(0);

const handle = () => {
    setCount(count + 1);
    setTimeout(() => {
       -  setCount(count + 2)
       +  setCount(count => count + 2) // 异步写法
   }, 0)
}

4) 直接定义一个变量进行使用,例如将定时器标识变量定义在函数组件外部(全局环境中)

又比如,如果不使用useState设置值(只是定义一个变量let count = 1; ),直接使用count = count + 1;修改值。不使用setCount修改值,那么每次取到的也是最新的count,因为不使用setCount函数组件就不会去更新,始终就是同一个闭包,因此也就取的是同一个值。

5)如果是使用useMemo或者useCallback这种hooks形成了闭包,那么还可以将所有的依赖项都添加到数组中,也能达到更新效果

function App() {
  return <Demo1 />
}
 
function Demo1(){
  const [num1, setNum1] = useState(1)
  const [num2, setNum2] = useState(10)
 
  const text = useMemo(()=>{
    return `num1: ${num1} | num2:${num2}`
  }, [num2])  // 此处只添加num2即可,因为num2和num1是存在于同一个闭包里,所以更新时是同步的
 
  function handClick(){
    setNum1(2)
    setNum2(20)
  }
 
  return (
    <div>
      {text}
      <div><button onClick={handClick}>click!</button></div>
    </div>
  )
}

注意:且如果多个依赖项具备同时变化的特性,那么只将其中一个依赖项放入数组中即可
 

参考文章:

参考1:简化版

出处2:详解版

示例3:demo演示版

猜你喜欢

转载自blog.csdn.net/BUG_CONQUEROR_LI/article/details/128232106