Table of Contents

1、概述

这一章,我们继续探讨闭包陷阱。如下案例所示,我们在上一章案例的基础之上,再新增了一个变量。我们希望这个新增的变量 step 能够控制 count 的累加步长。

我们按照上一章的思路,通过 setCount((prevCount) => prevCount + step) 来获取 count 的最新值

但是,step 又出问题了,点击按钮增加 step 的值,我们会发现,count 的累加步长并没有发生变化。

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

count 虽然能够拿到最新值,但是 `step` 又只能从闭包环境中拿上一次的值

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
const [step, setStep] = useState(1);
07
08
useEffect(() => {
09
const timer = setInterval(() => {
10
setCount((prevCount) => prevCount + step);
11
}, 1000);
12
13
return () => clearInterval(timer);
14
}, [])
15
16
return (
17
<RenderCount count={count} step={step} setStep={setStep} />
18
);
19
};

我们再也没有办法像 count 那样,绕过闭包环境去 Fiber 中获取 step 的最新值了。因此,根据上一章的结论,我可以通过将 step 作为 deps 依赖项的方式,每次 step 发生变化时,就重新覆盖旧的 effect 函数

那么,step 也能拿到最新值

预览
0Count
step: 1
正常运行

通过将 `step` 作为 `deps` 依赖项的方式,每次 `step` 发生变化时,就重新覆盖旧的 `effect` 函数

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
const [step, setStep] = useState(1);
07
08
useEffect(() => {
09
const timer = setInterval(() => {
10
setCount((prevCount) => prevCount + step);
11
}, 1000);
12
13
return () => clearInterval(timer);
14
}, [step])
15
16
return (
17
<RenderCount count={count} step={step} setStep={setStep} />
18
);
19
};

我们接下来要需要分析与思考一下,step 在逻辑中,step 是否应该作为依赖项?实际上不应该,因为在逻辑上来说,我们之所以起一个 useEffect,其核心目的就只是为了在组件渲染完成之后,定义一个定时器的作用,而如果我们为了获得 step 的最新值,而去将 step 作为依赖项,那么,我们每次点击按钮,都会重新定义一个定时器,这显然是不合理的

最终的结果就是,当我们快速点击 step 的控制按钮时,定时器会因为频繁取消而停下来

所以,增加依赖项虽然能解决获得最新值的问题,但是并不完美

2、逻辑值与状态值

状态值:定义在 useState 中,它的变化会导致 UI 发生变化

逻辑值:定义在 useRef 中,它不会触发 UI 的变化,但是会参与逻辑运算

很显然,在 useEffect 的逻辑中,step 是以一个逻辑值的身份,参与到逻辑运算中去的。通常情况下,我们能够非常容易的区分逻辑值与状态值。但是这里 step 又是一个特殊的情况,它既是一个逻辑值,又是一个状态值。因为在 UI 中也需要显示它的变化

于是,初学者在这里就很容易不知道怎么解决

一个非常巧妙的办法就是,将 step 一分为二,分别定义一个状态值,一个逻辑值

index.tsx
1
const [step, setStep] = useState(1);
2
const stepRef = useRef(1);

我们只需要在修改他们的时候,保证同步更新,就可以既满足逻辑值的需求,又满足状态值的需求

index.tsx
1
function setStepValue(value: number) {
2
setStep(value);
3
stepRef.current = value;
4
}

完整代码与演示效果如下所示

预览
0Count
step: 1
正常运行

将 `step` 按照状态值与逻辑值的标准一分为二,各司其职

index.tsx
render-count.tsx
01
import { useState, useEffect, useRef } from 'react';
02
import RenderCount from './render-count';
03
04
export default function Counter() {
05
const [count, setCount] = useState(0);
06
const [step, setStep] = useState(1);
07
const stepRef = useRef(1);
08
09
useEffect(() => {
10
const timer = setInterval(() => {
11
setCount((prevCount) => prevCount + stepRef.current);
12
}, 1000);
13
14
return () => clearInterval(timer);
15
}, [])
16
17
function setStepValue(value: number) {
18
setStep(value);
19
stepRef.current = value;
20
}
21
22
return (
23
<RenderCount count={count} step={step} setStep={setStepValue} />
24
);
25
};

3、总结

在 React 中,useEffect 的依赖项的思考是一件比较复杂的事情,许多人经常会滥用,认为只要在 effect 函数中用到的状态,都应该添加到依赖项中,实际上未必如此,我们要结合实际情况进行思考。

将状态按照其职能一分为二,在过去的版本中,是一个非常巧妙的解决方案。当然,这在代码实现上略微繁琐,因此在最新的版本中,React 为我们提供了一个更加简洁的解决方案,那就是 useEffectEvent

专栏首页
到顶
专栏目录