相信使用React hooks开发的小伙伴肯定或多或少都遇到过一些“奇怪”的“闭包陷阱”的场景(其实是符合逻辑的)。如下:
const testDemo = () => {
const [count, setCount] = useState(1)
useEffect(() => {
setInterval(() => {
console.log(count)
}, 1000)
//闭包陷阱
}, [])
const handleClick = () => {
setCount(count+1)
}
return (
click to add, count: {count}
)
}
我们期待在点击之后, 打印出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 click count add : {count}
可以不设置参数,那么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 (
{count}
)
}
// 正常的写法
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
}
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 (
{text}
)
}
注意:且如果多个依赖项具备同时变化的特性,那么只将其中一个依赖项放入数组中即可
参考1:简化版
出处2:详解版
示例3:demo演示版