React闭包陷阱,你是否遇到过状态数据一直保持不变的情况

React闭包陷阱

闭包(closure)是一个函数以及其捆绑的周边环境状态(lexical environment词法环境)的引用的组合。换而言之,闭包让开发者可以从内部函数访问外部函数的作用域。在 JavaScript 中,闭包会随着函数的创建而被同时创建。

​ 先看一个熟悉的闭包场景

for ( var i=0; i<5; i++ ) {
    setTimeout(()=>{
        console.log(i)
    }, 0)
}

​ 结果打印的都是5,预期想要打印的是0-4,使用闭包解决如下:

for (var i: number = 0; i < 5; i++) {
  ;(i =>
    setTimeout(() => {
      console.log(i)
    }, 0))(i)
}

​ 这个原理其实就是使用闭包,定时器的回调函数去引用立即执行函数里定义的变量,形成闭包保存了立即执行函数执行时 i 的值,异步定时器的回调函数才如我们想要的打印了顺序的值。下面提到的闭包陷阱和这也是类似的

1. useState闭包陷阱

​ 在useState中使用闭包,主要是因为useState的参数只会在组件挂载时执行一次。如果我们在useState中使用闭包,那么闭包中的变量值会被缓存,这意味着当我们在组件中更新状态时,闭包中的变量值不会随之更新。

观察如下一段代码:

import { useState } from 'react'

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}>Add</button>
      <button onClick={handleReset}>Reset</button>
    </div>
  )
}

export default Counter

​ 在 handleClick 函数中,使用了 useState 返回的 setCount 函数来更新 count 状态值。由于 setCount 是在 App 函数中定义的,而 handleClick 函数可以访问 App 函数中定义的变量和函数,因此 handleClick 函数形成了一个闭包,可以访问 App 函数中定义的 count 状态值和 setCount 函数。
​ 上面代码中,定义了handleClick函数,形成一个闭包缓存count,当我们点击Add时,1秒后执行setCount将count加一,在这1秒内无论你点击多少次按钮,count都只加1。这是因为在这1秒内,setCount所接受的count,一直都是还没有加1的count,即这个count仅仅是缓存的count。实际上就是在这1秒内,假如你点击十次,那么你将会执行十次setCount(count+1),但是count在这1秒内一直是同一个值。

React闭包陷阱,你是否遇到过状态数据一直保持不变的情况_第1张图片

解决办法:setCount函数可以接受一个回调函数作为参数

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

handleClick函数做如下更改:

const handleClick = () => {
   setTimeout(() => {
     setCount(currentCount => currentCount + 1)
   }, 1000)
 }

2. useEffect闭包陷阱

看如下一段代码

import { useEffect, useState } from 'react'
function Counter() {
  const [count, setCount] = useState(0)

  useEffect(() => {
    const timer = setInterval(() => {
      console.log(count)
    }, 1000)
    return () => clearInterval(timer)
  }, [])

  const handleClick = () => {
    setCount(count + 1)
  }

  return (
    

Count: {count}

) } export default Counter

上面代码中,我们在useEffect中开启定时器每一秒打印状态count值,但是即使我们点击按钮增加count值,但是定时器打印的count值一直都保存0不变

如图:

在这里插入图片描述

原因是, useEffect 只会在组件首次渲染时执行一次,因此闭包中的 count 变量始终是首次渲染时的变量,而不是最新的值。

解决方案:

  • 为展示的count打上ref标签,读取count值

  • 使用ahooks中的useLates

    • 代码:

      import { useEffect, useState } from 'react'
      import { useLatest } from 'ahooks'
      
      // useEffect 闭包陷阱
      function Counter() {
        const [count, setCount] = useState(0)
      
        const cntRef = useLatest(count)
      
        useEffect(() => {
          const timer = setInterval(() => {
            console.log(cntRef.current)
          }, 1000)
          return () => clearInterval(timer)
        }, [])
      
        const handleClick = () => {
          setCount(count + 1)
        }
      
        return (
          <div>
            <p>Count: {count}</p>
            <button onClick={handleClick}>Add</button>
          </div>
        )
      }
      
      export default Counter
      
    • 结果:

    React闭包陷阱,你是否遇到过状态数据一直保持不变的情况_第2张图片

  • 当count变化时,更新定时器

      useEffect(() => {
        const timer = setInterval(() => {
          console.log(count)
        }, 1000)
        return () => clearInterval(timer)
      }, [count])
    

你可能感兴趣的:(React,react.js,javascript,前端)