react hooks的闭包陷阱(setInterval不生效)问题

在使用react hooks时, 会遇到这样的问题

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还是初始化时的值,
并没有获取到最新的. 这就是闭包陷阱

怎么解决

使用useRef可以解决闭包陷阱的问题, 为什么?

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

同理, 我们如果这样浅拷贝, 也是可以获取到最新的值的

useEffect(() => {
        setInterval(() => {
            console.log(count)
        }, 1000)
        //闭包陷阱
    }, [])
    
const handleClick = () => {
        setCount((prevState) => {
            //浅拷贝
            return Object.assign(prevState, {
                name: 'ssdsd',
            })
        })
        setDate('yesterday')
    }
    
return (
        <div onClick={handleClick}>
            click to add, count: {count.a}, name:{count.name}
        </div>
    )

在点击后setInterval中的值也会更新, 因为本质上就是同一个内存空间的一个对象

或者这样, 也可以证明上述结论

const handleClick = () => {
        setCount((prevState) => {
            //浅拷贝
            let aaa = prevState
            aaa.name = 'sfss'
            return aaa
        })
        setDate('yesterday')
    }

还有一个类似问题

看到别人遇到的一个问题
参考

import React, {useState} from 'react'
let myInterval = null
const Interval = () => {
  const [count, setCount] = useState(0)
 
  function interval () {
    myInterval = setInterval(() => {
      console.log(count) // 这里永远都是0,而且页面虽然没有任何变化,但是这里一直在执行
      if (count >= 5) {
        clearInterval(myInterval)
        return;
      }
      setCount(count + 1)
    }, 1000)
  }
  return (
    <div onClick={interval}>click me to count {count}</div>
  )
}
 
export default React.memo(Interval)

点击后, 调用interval函数, 里面的setInterval每次都会setCount, 期望打印出的count依次递增, 大于5时clearInterval.

但是实际上, 根据我们上面的分析, 每次setCount之后, 页面虽然重新渲染, 但是正在运行的setInterval仍然是第一次的(闭包), 上下文环境也是第一次的, 因此count永远都是0.

解决办法

使用useEffect在页面更新时清除interval

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>

总结

解决闭包陷阱的两种办法

  • 清除重建
  • 使用对象的引用, 直接获取对象本身的数据
  • 1
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值