Table of Contents

1、概述

当 2 个不同的函数作用域中,共同的访问了同一个变量时,就会形成闭包。

很显然,在函数组件中,当我们使用 useEffect 时,就就非常容易达到闭包的条件,例如这段代码,我们在 useEffect 中使用了 useState 定义的变量

index.tsx
1
function Counter() {
2
const [count, setCount] = useState(0);
3
4
useEffect(() => {
5
console.log(count);
6
}, [count]);
7
8
...
9
}

此时,外层的 Counter 函数,就与内层的匿名函数 effect 形成了闭包,他们访问了共同的变量 count,因此,一个包含 count 的闭包就形成了。

闭包环境是否稳定

许多初学者朋友会因为一些误导,认为闭包一旦形成,就不会被回收,实际上并非如此,只有保持了稳定引用的闭包环境,才会被保留下来

在 React 中,由于 Counter 函数一直在因为状态的变化而重复执行,因此,闭包环境也一直在重复的创建与销毁,那么,什么因素是决定闭包环境是否稳定关键呢?

那就是 effect 函数的引用是否被保留。

index.tsx
1
useEffect(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

预览
0Count
陷阱触发 (闭包旧值)

使用了 setCount(count + 1)。由于依赖数组为空,闭包环境保留,effect 内部只能拿到初始渲染时的 count (0)

index.tsx
render-count.tsx
01
import { useState, useEffect } from 'react';
02
import RenderCount from './render-count';
03
04
export default function Counter() {
05
const [count, setCount] = useState(0);
06
07
useEffect(() => {
08
const timer = setInterval(() => {
09
setCount(count + 1);
10
}, 1000);
11
12
return () => clearInterval(timer);
13
// 依赖没有改变,因此闭包环境稳定,effect 内部只能拿到初始渲染时的 count (0)
14
}, [])
15
16
return (
17
<RenderCount count={count} />
18
);
19
};

为了修复这个问题,我们要做出的改变就是,不能从当前的闭包环境中去拿上一次的值来计算。我们可以使用如下语法,直接从 Fiber 节点中直接获取最新值

INFO

setCount 会将最新值保存在 Fiber 节点中

index.tsx
1
setCount((prevCount) => prevCount + 1);

修复之后,演示效果如下所示

预览
0Count
正常运行 (函数式更新)

使用了 setCount(prev => prev + 1),总是获取最新状态值

index.tsx
render-count.tsx
01
import { useState, useEffect } from 'react';
02
import RenderCount from './render-count';
03
04
export default function Counter() {
05
const [count, setCount] = useState(0);
06
07
useEffect(() => {
08
const timer = setInterval(() => {
09
// 使用函数式更新,从 Fiber 节点中获取最新值
10
setCount((prevCount) => prevCount + 1);
11
}, 1000);
12
13
return () => clearInterval(timer);
14
}, [])
15
16
return (
17
<RenderCount count={count} />
18
);
19
};
专栏首页
到顶
专栏目录