当 2 个不同的函数作用域中,共同的访问了同一个变量时,就会形成闭包。
很显然,在函数组件中,当我们使用 useEffect 时,就就非常容易达到闭包的条件,例如这段代码,我们在 useEffect 中使用了 useState 定义的变量
1function Counter() {2const [count, setCount] = useState(0);34useEffect(() => {5console.log(count);6}, [count]);78...9}
此时,外层的 Counter 函数,就与内层的匿名函数 effect 形成了闭包,他们访问了共同的变量 count,因此,一个包含 count 的闭包就形成了。
许多初学者朋友会因为一些误导,认为闭包一旦形成,就不会被回收,实际上并非如此,只有保持了稳定引用的闭包环境,才会被保留下来
在 React 中,由于 Counter 函数一直在因为状态的变化而重复执行,因此,闭包环境也一直在重复的创建与销毁,那么,什么因素是决定闭包环境是否稳定关键呢?
那就是 effect 函数的引用是否被保留。
1useEffect(effect, deps);
这里需要特别注意仔细理解,effect 函数,在 useEffect 执行时,仅仅只是定义,在 useEffect 内部,他会被存储在内部的 Fiber 节点上。一旦 effect 的引用被稳定的存储,那么,闭包环境就会稳定下来
但是,Fiber 节点上存储的 effect 会因为 deps 的变化而被新的 effect 函数覆盖。
这段话很重要:每当组件函数执行时, useEffect 必定也会跟着执行。此时会重新创建新的 effect 函数,如果 deps 依赖项发生了变化,那么新的 effect 函数会覆盖掉旧的 effect 函数。
当旧的 effect 函数被新的 effect 函数覆盖时,上一次的闭包环境就彻底失去了引用了,此时生效的就是新的闭包环境。
因此,如果函数的执行次数、与 deps 依赖的变化次数严格一致,那么,我们的代码逻辑就不会受到闭包问题的影响。当 effect 函数执行时,我们总能拿到最新的状态值,因为它是最新创建的 effect 函数
如果 deps 依赖的变化次数,少于函数的执行次数,那么,effect 函数的逻辑,就有可能一直处于上一次的闭包环境中,而拿不到最新的状态值。这就是闭包陷阱
如下案例所示,我们希望 count 能够按秒累加,但是,由于闭包陷阱的影响,count 始终只能拿到初始渲染时的值,而无法拿到最新的值,所以最终显示的值始终是 1
使用了 setCount(count + 1)。由于依赖数组为空,闭包环境保留,effect 内部只能拿到初始渲染时的 count (0)
01import { useState, useEffect } from 'react';02import RenderCount from './render-count';0304export default function Counter() {05const [count, setCount] = useState(0);0607useEffect(() => {08const timer = setInterval(() => {09setCount(count + 1);10}, 1000);1112return () => clearInterval(timer);13// 依赖没有改变,因此闭包环境稳定,effect 内部只能拿到初始渲染时的 count (0)14}, [])1516return (17<RenderCount count={count} />18);19};
为了修复这个问题,我们要做出的改变就是,不能从当前的闭包环境中去拿上一次的值来计算。我们可以使用如下语法,直接从 Fiber 节点中直接获取最新值
setCount会将最新值保存在 Fiber 节点中
1setCount((prevCount) => prevCount + 1);
修复之后,演示效果如下所示
使用了 setCount(prev => prev + 1),总是获取最新状态值
01import { useState, useEffect } from 'react';02import RenderCount from './render-count';0304export default function Counter() {05const [count, setCount] = useState(0);0607useEffect(() => {08const timer = setInterval(() => {09// 使用函数式更新,从 Fiber 节点中获取最新值10setCount((prevCount) => prevCount + 1);11}, 1000);1213return () => clearInterval(timer);14}, [])1516return (17<RenderCount count={count} />18);19};