详解 React 全新 API useEffectEvent

AI 概述
1、2、闭包陷阱3、新的问题4、状态的逻辑属性与 UI 属性5、useEffectEvent 1、 useEffectEvent 这个 API 的学习稍微有点绕,因此需要一点学习成本。 useEffectEvent 是为了解决 useEffect 中的依赖项问题而设计出来的。这基于一个重要的前提:React 官方团队认为,我们应该将所有在 useEffect...
目录
文章目录隐藏
  1. 1、概述
  2. 2、闭包陷阱
  3. 3、新的问题
  4. 4、状态的逻辑属性与 UI 属性
  5. 5、useEffectEvent

详解 React 全新 API useEffectEvent

1、概述

useEffectEvent 这个 API 的学习稍微有点绕,因此需要一点学习成本。

useEffectEvent 是为了解决 useEffect 中的依赖项问题而设计出来的。这基于一个重要的前提:React 官方团队认为,我们应该将所有在 useEffect 中使用到的 state 与 props 都作为 useEffect 的依赖项。

✓包括我们用到的 linter 规则中,也会要求我们把所有在 useEffect 中使用到的 state 与 props 都作为 useEffect 的依赖项。否则就会爆出警告。当然,这种观点与真实情况的项目开发中存在差异,在过去,我们都会通过修改 linter 的规则来忽略这种警告。

但是实际情况是,这样做之后,一方面是有可能会导致依赖项变得更冗余,另外一方面,这些依赖项中还可能存在一些我们并不希望作为依赖项的值。主要有两种情况

  1. 我们需要依赖项的变化,去重新执行 useEffect 中的回调函数;
  2. 我们不需要依赖项的变化,去重新执行 useEffect 中的回调函数。

对于第一种情况,这是合理的。但是对于第二种情况,就有可能会出现一些意想不到的 bug 或者冗余的计算。

所以在过往的开发中,对于我个人而言,在使用 useEffect 时,都会非常谨慎的去设计 useEffect 的依赖项,去避免这些问题的存在。

如果这样会存在问题,那么官方文档为什么还要建议我们把所有在 useEffect 中使用到的 state 与 props 都作为 useEffect 的依赖项呢?

2、闭包陷阱

在学习过 JS 核心进阶之后,我们都知道,如果我们在两个不同的函数作用域中,使用了同一个变量,那么此时就会形成闭包。

由于函数组件本身是一个函数,如果我们在 useEffect 的回调函数中使用了组件内部声明的 state 或者 props,那么此时就会形成闭包。由于类似的语法大量存在,因此闭包的存在在 React 中会显得非常普遍。

当然,并不是有了闭包就会导致问题的发生,知道了这些基础知识之后,我们来通过一个案例,来讲解闭包陷阱是怎么回事。这和 useEffect 的底层实现有不可分割的关系。

我们来看一下这个案例:

闭包陷阱

import { useState, useEffect } from 'react';
import Button from 'components/ui/button';

export default function Timer() {
  const [count, setCount] = useState(0);
  const [increment, setIncrement] = useState(1);

  useEffect(() => {
    const id = setInterval(() => {
      setCount(c => c + increment);
    }, 1000);
    return () => {
      clearInterval(id);
    };
  }, []);

  function incrementHandler() {
    setIncrement(i => i + 1);
  }

  function decrementHandler() {
    setIncrement(i => i - 1);
  }

  function resetHandler() {
    setCount(0);
  }

  return (
    <div className='p-4'>
      <div className='flex items-center justify-between'>
        <div className='text-2xl font-bold font-din'>
          Counter: {count}
        </div>

        <Button onClick={resetHandler}>Reset</Button>
      </div>
      <hr />
      <div className='flex items-center gap-2'>
        Every second, increment by:
        <Button disabled={increment === 0} onClick={decrementHandler}>–</Button>
        <span className='text-lg font-din'>{increment}</span>
        <Button onClick={incrementHandler}>+</Button>
      </div>
    </div>
  );
}

该案例中,我们设计了一个定时器,定时器每秒钟执行一次,并以累加的方式更新 counter 的值。

同时,我们设计了另外一个值,用于控制定时器每次累加的值。当我们点击按钮时,可以增加或者减少这个值。

在代码中,我们使用 useEffect 来定义定时器。

useEffect(() => {
  const id = setInterval(() => {
    setCount(c => c + increment);
  }, 1000);
  return () => {
    clearInterval(id);
  };
}, []);

这里注意观察依赖项,由于我们并不需要重复执行 useEffect 中的回调函数,仅需要在初始化时定义一下定时器即可,因此我们传入了一个空数组作为依赖项。这也就意味着,在后续的渲染过程中,useEffect 中的回调函数不会被重复执行。

但是,这样的写法就会存在一个问题:当我们点击递增或者递减按钮时,increment 的值虽然会发生变化,但定时器的递增值却不会变化。他依然会按照初始化时的 increment 值进行累加。

这就是闭包陷阱。

我们来仔细分析一下原因。首先,函数作用域 Timer 与 setInterval 的回调函数中,都使用了同一个变量 increment。因此,他们之间形成了闭包。

而此时,由于 useEffect 的依赖项为空,因此 useEffect 的回调函数会在初始化之后,就被缓存到内存中。在后续组件的多次重新渲染中,该缓存函数会一直存在,并且不会被更新。因此,在初始化时,所形成的闭包环境是始终稳定的。

所以,每次 setInterval 的回调函数执行时,使用的 increment 值都是初始化时的值。尽管后续 increment 的值已经发生了变化,但闭包环境中的 increment 值并没有发生变化。

因此,为了解决这个闭包陷阱的问题,我们只需要做一个很简单的修改,那就是破坏闭包环境的稳定性即可。也就是将依赖项设置为 increment

useEffect(() => {
  const id = setInterval(() => {
    setCount(c => c + increment);
  }, 1000);
  return () => {
    clearInterval(id);
  };
}, [increment]);

由于依赖项发生变化,此时每当 increment 的值发生变化时,useEffect 的回调函数都需要被重新执行。因此,每次函数组件重新执行时,useEffect 的回调函数都需要在依赖项发生变化时重新赋值,从而让每次执行过程中形成的闭包环境无法稳定保存下来,这样每次都会读取到最新的 increment 值。

3、新的问题

通过新增依赖项的方式,闭包陷阱的问题虽然解决了。但是,我们最开始之所以没有把 increment 设置为依赖项,是因为我们并不需要依赖项的变化,去重新执行 useEffect 中的回调函数。这也就意味着,我们希望 useEffect 中的回调函数在组件的整个生命周期中,只执行一次。

第二个案例中新增加的问题就是,useEffect 的回调函数的多次执行违背了我们的初衷。带来的运行效果就是:当我们快速重复点击递增或者递减按钮时,Counter 值的变化会在点击的过程中停下来。这并不符合我们想要的效果。

useEffect 的回调函数的多次执行

原因就是 useEffect 的回调函数在多次重复执行过程中,会反复的取消与创建定时器

那到底要怎么办才能更好的解决这个问题呢?

4、状态的逻辑属性与 UI 属性

在过往的开发经验中,我深刻的明白,大多数朋友会滥用 useState。因此在 React 知命境界的教学中,我们一直强调要分清楚,你定义的这个变量,是状态值,还是逻辑值。

  • 状态值:用于驱动 UI 的变化;
  • 逻辑值:不直接驱动 UI 的变化,而是参与逻辑运算。

状态值我们用 useState 来定义。

逻辑值我们用 useRef 来定义。

而如果此时,我们需要定义的这个变量,他既是状态值,又是逻辑值,事情就麻烦了。这就会非常容易导致闭包陷阱的产生。正如上面那个案例的 increment 变量,他既是状态值,又是逻辑值。

我们希望他以逻辑值的身份参与到 useEffect 的回调函数中,而不是以状态值的身份去添加到依赖项中

因此,在过往的解决方案中,我们为了绕开闭包陷阱,但是又不想把 increment 作为依赖项,我们就会把这个变量一分为二,分别定义一个状态值,一个逻辑值。

// 状态值驱动 UI 变化
const [increment, setIncrement] = useState(1);

// 逻辑值参与 useEffect 的回调函数逻辑运算
const incrementRef = useRef(1);

然后在更新时,保证状态值与逻辑值的同步更新。

setIncrement(i => i + 1);
incrementRef.current += 1;

这样,我们就可以保证在 useEffect 的回调函数中,使用的 increment 值始终是最新的值,又不用把 increment 作为依赖项。

useEffect 的回调函数

完整的代码如下所示:

import { useState, useEffect, useRef } from 'react';
import Button from 'components/ui/button';

export default function Timer() {
  const [count, setCount] = useState(0);
  const [increment, setIncrement] = useState(1);
  const incrementRef = useRef(1);

  useEffect(() => {
    const id = setInterval(() => {
      setCount(c => c + incrementRef.current);
    }, 1000);
    return () => {
      clearInterval(id);
    };
  }, []);

  function incrementHandler() {
    setIncrement(i => i + 1);
    incrementRef.current += 1;
  }

  function decrementHandler() {
    setIncrement(i => i - 1);
    incrementRef.current -= 1;
  }

  function resetHandler() {
    setCount(0);
  }

  return (
    <div className='p-4'>
      <div className='flex items-center justify-between'>
        <div className='text-2xl font-bold font-din'>
          Counter: {count}
        </div>

        <Button onClick={resetHandler}>Reset</Button>
      </div>
      <hr />
      <div className='flex items-center gap-2'>
        Every second, increment by:
        <Button disabled={increment === 0} onClick={decrementHandler}>–</Button>
        <span className='text-lg font-din'>{increment}</span>
        <Button onClick={incrementHandler}>+</Button>
      </div>
    </div>
  );
}

5、useEffectEvent

过往的这种解法,虽然完美的解决了问题,但是在写法上理解起来就有点抽象。明明是一个值,却要拆分成两个变量来定义,这就会导致理解成本的增加。甚至许多 React 开发者都没有听说过状态值和逻辑值的概念。

因此,React 还需要一种更加直观的解决方案,来避免将变量一分为二。这就是 useEffectEvent

我们可以把逻辑运算的部分,从 useEffect 的回调函数中抽离出来,并使用 useEffectEvent 来定义。这样,在 useEffect 中,我们也不需要把逻辑值作为依赖项了。完整的代码如下所示

import { useState, useEffect, useEffectEvent } from 'react';
import Button from 'components/ui/button';

export default function Timer() {
  const [count, setCount] = useState(0);
  const [increment, setIncrement] = useState(1);

  const incrementEvent = useEffectEvent(() => {
    setCount(c => c + increment);
  });

  useEffect(() => {
    const id = setInterval(incrementEvent, 1000);
    return () => clearInterval(id);
  }, []);

  function incrementHandler() {
    setIncrement(i => i + 1);
  }

  function decrementHandler() {
    setIncrement(i => i - 1);
  }

  function resetHandler() {
    setCount(0);
  }

  return (
    <div className='p-4'>
      <div className='flex items-center justify-between'>
        <div className='text-2xl font-bold font-din'>
          Counter: {count}
        </div>

        <Button onClick={resetHandler}>Reset</Button>
      </div>
      <hr />
      <div className='flex items-center gap-2'>
        Every second, increment by:
        <Button disabled={increment === 0} onClick={decrementHandler}>–</Button>
        <span className='text-lg font-din'>{increment}</span>
        <Button onClick={incrementHandler}>+</Button>
      </div>
    </div>
  );
}

在 useEffectEvent 的回调函数中,我们能够读取到最新的状态值(state or props),因此我们可以放下闭包陷阱的担忧放心使用。

不过使用时我们需要注意如下几点

  1. useEffectEvent 是抽取的useEffect回调函数中的逻辑,因此只能在 effects 内部调用,而不能在 effects 外部调用;
  2. 建议仅用于处理一个状态同时具备状态值与逻辑值的情况,如果该变量只有逻辑值的身份,那么应该使用 useRef 来定义。

以上关于详解 React 全新 API useEffectEvent的文章就介绍到这了,更多相关内容请搜索码云笔记以前的文章或继续浏览下面的相关文章,希望大家以后多多支持码云笔记。

「点点赞赏,手留余香」

1

给作者打赏,鼓励TA抓紧创作!

微信微信 支付宝支付宝

还没有人赞赏,快来当第一个赞赏的人吧!

声明:本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如若内容造成侵权/违法违规/事实不符,请将相关资料发送至 admin@mybj123.com 进行投诉反馈,一经查实,立即处理!
重要:如软件存在付费、会员、充值等,均属软件开发者或所属公司行为,与本站无关,网友需自行判断
码云笔记 » 详解 React 全新 API useEffectEvent

发表回复