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秒内一直是同一个值。
解决办法: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 (
<div>
<p>Count: {count}</p>
<button onClick={handleClick}>Add</button>
</div>
)
}
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
-
结果:
-
-
当count变化时,更新定时器
useEffect(() => { const timer = setInterval(() => { console.log(count) }, 1000) return () => clearInterval(timer) }, [count])