React系列- 图解:React Hook闭包问题
前言:最近参与了同事的一个分享,其中里面的一个闭包问题引起了我的兴趣,由于时间问题,分享上并没有详解这里面的“前世今生”,带着好奇的心区学习了的这里面的一点一滴(虽然我卷,但我还是菜!~) 好了,接下来说一下具体的问题是什么?
React Hooks 是什么?
关于react Hooks的介绍,可以看我的其他文章:juejin.cn/post/723905…
React Hooks 的存储的数据结构
- 首先,了解下Hook的结构 hook的数据结构是一个链表的结构,react组件的hooks会按照顺序存储在下面的链表结构中,这也是为什React不允许在if等条件语句中使用hooks,会改变hooks的前后关系引起不必要的问题
export type Hook = {
memoizedState: any,
baseState: any,
baseQueue: Update<any, any> | null,
queue: any,
next: Hook | null,
};
- 举例说明
Demo
组件中声明了三个hooks,分别是useState
和另外两个useEffect
const Demo = () => {
const [a, setA] = useState(1)
useEffect(() => {
setInterval(() => {
console.log("hook state:", a)
}, 1000)
}, [])
useEffect(() => {
window.test2= a;
let timer = setInterval(() => {
console.log("window.test state:", window.test)
}, 1000)
return () => clearInterval(timer)
}, [])
return <Card className={styles.card_content}>
<Button onClick={() => setA(v => v + 1)}>点击hook A +1 </Button>
<Button onClick={() => window.test += 1}>点击window +1 </Button>
{/* <div className={styles.card_body}>我是内容1</div>
<div className={styles.card_body}>我是内容2</div> */}
</Card>
}
查看Demo
的react数据结构
React Hooks的闭包陷阱
-
什么是React Hook闭包陷阱? 我个人根据现象的理解就是React Hook中使用的了对之前的state对象的引用,导致了该引用变量不会随组件的重新渲染而更新。(都是些什么乱七八糟的表述,感觉都很抽象又官方)\
下面我们用例子说明 还是上面
Demo
的代码,在组件挂载后,useEffect
中开启了一个setInterval
定时器并每隔一秒输出一次state a的值。a的初始值为1const Demo = () => { const [a, setA] = useState(1) useEffect(() => { setInterval(() => { console.log("hook state:", a) }, 1000) }, []) console.log("Demo-State-A:", a) // ...其他代码 return <Card className={styles.card_content}> <Button onClick={() => setA(v => v + 1)}>点击hook A +1 </Button> {/** 其他代码... */} </Card> }
浏览器打印看下效果:会持续输出
点击点击hook A +1
按钮,观察输出:点击了两次,组件的state a状态变更为3,但是定时器的输出仍为1
,这种现象就是react 闭包陷阱。
- 如何产生的?
- 讲了这么多,那么这个闭包陷阱是如何产生的呢?其中最关键的原因就是React的更新逻辑,React更新时,生成一个新的states对象,而不是在原来的对象上修改。这就导致在闭包场景下会存在两个state对象,一个是老的state(以下称
oldState
),一个是新的state对象(以下称newState
),因为oldState
被闭包引用,所以一直没有被释放。所以呢,闭包中输出的值一直是oldState
的值,而不是react更新后新的newState
的值。这就解释了为什么打印的都是最初的值。 讲了这么多,可能会比较迷糊,下面我们用图例解释说明:react 更新后,会创建一个新的state对象(
newState
),前一次的State(oldState
)对象若无引用将会被垃圾回收,详细流程见下图
明白了上面的更新过程,我们就知道了为什么造成了闭包。以下图详解。
闭包陷阱的解决方案
知道了上面的闭包原因,解决办法就会简单多了
-
添加必要依赖 通过hooks(useEffect、useCallback等)的第二个参数:
deps
,保证每次更新时替换hook缓存的函数,读取到的是最新的state. -
使用useRef 使用useRef,是由于useRef创建的对象,不受组件的影响,里面的值变化后,其本身的内存地址是不变的,所以可以被读取到。 代码示例:
const Demo = () => { const [a, setA] = useState(1) const ref = useRef() ref.current= a; useEffect(() => { setInterval(() => { console.log("hook state:", ref.current) }, 1000) }, [a]) console.log("Demo-State-A:", a) return <Card className={styles.card_content}> <Button onClick={() => setA(v => v + 1)}>点击hook A +1 </Button> </Card> }
打印测试结果,发现符合预期
- 使用组件之外的变量(跟useRef一个道理)
该方式就是通过使用比如
window
、cookie
、上层组件的对象或context
等引用不变的对象,都可以解决该问题。其原理跟useRef
是一样的。
结语
由于文笔的原因,可能不是那么生动有趣,但是本着认真的态度,希望这篇文章还是能给读者带来一些帮助。文中有不对之处,欢迎大家指正!~