在使用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>
总结
解决闭包陷阱的两种办法
- 清除重建
- 使用对象的引用, 直接获取对象本身的数据