这一章,我们继续探讨闭包陷阱。如下案例所示,我们在上一章案例的基础之上,再新增了一个变量。我们希望这个新增的变量 step 能够控制 count 的累加步长。
我们按照上一章的思路,通过 setCount((prevCount) => prevCount + step) 来获取 count 的最新值
但是,step 又出问题了,点击按钮增加 step 的值,我们会发现,count 的累加步长并没有发生变化。
count 虽然能够拿到最新值,但是 `step` 又只能从闭包环境中拿上一次的值
01import { useState, useEffect } from 'react';02import RenderCount from './render-count';0304export default function Counter() {05const [count, setCount] = useState(0);06const [step, setStep] = useState(1);0708useEffect(() => {09const timer = setInterval(() => {10setCount((prevCount) => prevCount + step);11}, 1000);1213return () => clearInterval(timer);14}, [])1516return (17<RenderCount count={count} step={step} setStep={setStep} />18);19};
我们再也没有办法像 count 那样,绕过闭包环境去 Fiber 中获取 step 的最新值了。因此,根据上一章的结论,我可以通过将 step 作为 deps 依赖项的方式,每次 step 发生变化时,就重新覆盖旧的 effect 函数
那么,step 也能拿到最新值
通过将 `step` 作为 `deps` 依赖项的方式,每次 `step` 发生变化时,就重新覆盖旧的 `effect` 函数
01import { useState, useEffect } from 'react';02import RenderCount from './render-count';0304export default function Counter() {05const [count, setCount] = useState(0);06const [step, setStep] = useState(1);0708useEffect(() => {09const timer = setInterval(() => {10setCount((prevCount) => prevCount + step);11}, 1000);1213return () => clearInterval(timer);14}, [step])1516return (17<RenderCount count={count} step={step} setStep={setStep} />18);19};
我们接下来要需要分析与思考一下,step 在逻辑中,step 是否应该作为依赖项?实际上不应该,因为在逻辑上来说,我们之所以起一个 useEffect,其核心目的就只是为了在组件渲染完成之后,定义一个定时器的作用,而如果我们为了获得 step 的最新值,而去将 step 作为依赖项,那么,我们每次点击按钮,都会重新定义一个定时器,这显然是不合理的
最终的结果就是,当我们快速点击 step 的控制按钮时,定时器会因为频繁取消而停下来
所以,增加依赖项虽然能解决获得最新值的问题,但是并不完美
状态值:定义在 useState 中,它的变化会导致 UI 发生变化
逻辑值:定义在 useRef 中,它不会触发 UI 的变化,但是会参与逻辑运算
很显然,在 useEffect 的逻辑中,step 是以一个逻辑值的身份,参与到逻辑运算中去的。通常情况下,我们能够非常容易的区分逻辑值与状态值。但是这里 step 又是一个特殊的情况,它既是一个逻辑值,又是一个状态值。因为在 UI 中也需要显示它的变化
于是,初学者在这里就很容易不知道怎么解决
一个非常巧妙的办法就是,将 step 一分为二,分别定义一个状态值,一个逻辑值
1const [step, setStep] = useState(1);2const stepRef = useRef(1);
我们只需要在修改他们的时候,保证同步更新,就可以既满足逻辑值的需求,又满足状态值的需求
1function setStepValue(value: number) {2setStep(value);3stepRef.current = value;4}
完整代码与演示效果如下所示
将 `step` 按照状态值与逻辑值的标准一分为二,各司其职
01import { useState, useEffect, useRef } from 'react';02import RenderCount from './render-count';0304export default function Counter() {05const [count, setCount] = useState(0);06const [step, setStep] = useState(1);07const stepRef = useRef(1);0809useEffect(() => {10const timer = setInterval(() => {11setCount((prevCount) => prevCount + stepRef.current);12}, 1000);1314return () => clearInterval(timer);15}, [])1617function setStepValue(value: number) {18setStep(value);19stepRef.current = value;20}2122return (23<RenderCount count={count} step={step} setStep={setStepValue} />24);25};
在 React 中,useEffect 的依赖项的思考是一件比较复杂的事情,许多人经常会滥用,认为只要在 effect 函数中用到的状态,都应该添加到依赖项中,实际上未必如此,我们要结合实际情况进行思考。
将状态按照其职能一分为二,在过去的版本中,是一个非常巧妙的解决方案。当然,这在代码实现上略微繁琐,因此在最新的版本中,React 为我们提供了一个更加简洁的解决方案,那就是 useEffectEvent