深圳幻海软件技术有限公司 欢迎您!

重点来了,UseEffect如何让使用者在函数组件中执行副作用操作

2023-02-28

作者|这波能反杀丶来源|这波能反杀(ID:keepKilling)序当了二十多天废物,今天开始更新,让大家久等了。花了挺长的时间优化文章UI细节,进一步提高阅读体验,大家多多感受一下.useEffect是一个难以掌握的知识点。许多人对它半知半解,因此他们觉得函数式组件不受控制。除了本身难以理解之外,

作者 |这波能反杀丶

来源 |这波能反杀(ID:keepKilling)

当了二十多天废物,今天开始更新,让大家久等了。花了挺长的时间优化文章 UI 细节,进一步提高阅读体验,大家多多感受一下.

useEffect 是一个难以掌握的知识点。许多人对它半知半解,因此他们觉得函数式组件不受控制。

除了本身难以理解之外,React 还提供了一个类似的 hook:useLayoutEffect 来增加学习难度,对于新手来说,这可要了老命了。

许多朋友试图利用 class 语法中的生命周期来类比理解 useEffect,因为官方文档就是这么引导的,那么他们多半会陷入一些误区,因此,学习之前,大家需要明确的是,生命周期函数与 useEffect 是不同的。

要充分理解并使用该方法,你需要对闭包、同步、异步、事件循环等基础概念有清晰认知。

一、概念

useEffect 可以让使用者在函数组件中执行副作用操作。

那什么是副作用操作呢?

在 React 中,由 state 的变化导致 UI 发生变化的过程是正常操作,其他操作行为:例如数据请求、直接手动修改 DOM 节点、直接操作页面「修改页面标题等」、记录日志等都是副作用操作。

副作用操作是相对于操作 state 而言的。

每一次因为 state 的改变,都有一次对应副作用函数的执行时机。如果 state 多次改变,那么就有多次对应副作用的执行时机。

例如:我希望记录点击的次数。

该次数不仅要在页面上显示,也要在页面标题中显示。

我们就可以给出如下代码来实现需求。

import { useState, useEffect } from 'react';
function Example() {
  const [count, setCount] = useState(0);
  useEffect(() => {
    // Update the document title using the browser API
    document.title = `You clicked ${count} times`;
  });
  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>
        Click me
      </button>
    </div>
  );
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.

在该例子中,修改页面标题的行为是副作用行为,因此我们可以直接使用 useEffect 来定义它。useEffect 的第一个参数为一个回调函数,该回调函数就是我们上面说的副作用函数「effect」,我们想要执行的副作用逻辑都写在该函数中。

二、语法

// 中括号表示参数可选
useEffect(effct[, deps])
  • 1.
  • 2.

useEffect 是 React 提供的 Hook,它能够帮助我们定义 effect 函数。

第一个参数就是副作用函数 effect。

第二个参数表示依赖项,是一个可选参数。当不传入该参数时,每次 UI 渲染 effect 函数都会执行。

但是大多数时候我们并不想任何 state 的变化都一定要执行 effect 函数,这个时候我们可以传入依赖项数组。使用时请确保依赖项数组中为 state/props 的值,表示 effect 只会响应依赖项中状态的变化。

如果你在 useEffect 中传入与 state 无关的数据,effect 不会响应它们。

只有当依赖项中是 state 发生变化时,effect 才会与之对应的执行。

不同的 state 数据变化通常对应不同的副作用操作。因此我们可以在函数组件中,定义多个 effect。

function Demo() {
  const [count, setCount] = useState(0)
  const [show, setShow] = useState(false)  
  useEffect(() => {
    // do something
  }, [count]) 
  useEffect(() => {
    // do other something
  }, [show])
  ...
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.

除此之外,我们还可以传入空数组作为依赖项,用于表示依赖项不会发生变化。因此,空数组对应的 effect,就只会在初始化时执行一次,以后就再也不会执行了。

我们通常利用这个特性完成一些初始化工作,例如请求页面数据。

const [list, setList] = useState(0);
// DOM渲染完成之后 effect 函数执行
useEffect(() => {
  recordListApi().then(res => {
    setList(res.data);
  })
}, []);
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.

三、清除副作用

有的时候,副作用函数 effect 执行会留下一些痕迹,因此 useEffect 提供了一种清除副作用的方式。

effect 与 clear effect 是一一对应的紧密关系。因此,我们可以定义一个回调函数由 effect 执行时返回,该函数就是 clear effect 函数。

useEffect(() => {
  // dosomething 
  // 定义 clear effect 函数
  return () => {
    // clear something
  }
}, [])
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.

这里一定要注意该函数与 class 组件中的 componentWillUnmount 的区别,官方文档中的案例存在一定的误导性。如果 deps 传入空数据,则两者是类似的,否则他们完全不一样,effect 与 clear effect 都有可能执行多次。

clear effect 在下次 effect 执行之前执行,也会在组件销毁之前执行一次。

我们可以借助该特性实现一个防抖的案例。

例如我们要实现一个搜索框的功能。文字输入过程中会自动发起搜索请求。为了防止请求发送过于频繁,在高频输入时,不发送接口请求,如果超过了 500ms 下一次输入事件还没有发生,那么就自动请求一次。

实现代码如下:

import { useEffect, useState } from 'react'
export default function EffectDemo() {
  const [text, setText] = useState('')
  useEffect(() => {
    let timer =  setTimeout(() => {
      console.log('发送搜索请求')
    }, 500)
    return () => {
      console.log('清除定时器')
      clearTimeout(timer)
    }
  }, [text])
  return (
    <div>
      <input type="text" placeholder='请输入内容...' onChange={(e) => setText(e.target.value)} />
    </div>
  )
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.

我们在 effect 中定义了定时器,作为延迟操作:500ms 后执行请求逻辑。如果下一次 text 快速发生变化,clear effect 执行会清除掉上一次定义的定时器任务,那么请求逻辑就不会执行。

只有下一次 text 的改变超过了 500ms 时,定时器任务才会如期执行。

执行顺序为:

四、案例

在学习和理解 effect 的含义时,我们知道 state 的变化引发 UI 重新渲染,UI 渲染完成之后会执行 effect。

然而在真实实践时,我们往往是知道自己要执行的副作用逻辑是什么,难的是需要我们自己去设计合理的 state。不合理的设计会让程序变得复杂。

现在我们要来实现下面的动画效果:

  1. 点击红色画布,白色方块执行第一段动画,并显示执行日志。
  2. 执行完后紧接着执行第二段动画回到圆点,并显示执行日志。
  3. 在白色方块执行动画的过程中点击事件无效:点击不影响动画的执行,结束之后重新生效。

这个效果的实现,最重要的是对于几个状态的设计。

首先,我们需要用一个状态来表示第一段动画的执行与否 anime01。

其次,我们需要用一个状态来表示第二段动画的执行与否 anime02。

最后,我们也可以使用一个额外的状态来判断整个过程是否已经执行完毕 stoped。

重点思考该状态的特性,与存在的必要性。

在实现该逻辑中,我们只需要知道每一个运动的结束时间点,并修改对应的状态即可。

例如:第一段动画执行结束,修改 anime02 为true。

完整代码如下:

import { useState, useRef, useEffect } from 'react';
// @ts-ignore
import anime from 'animejs';
import './style.scss';
export default function AnimateDemo() {
  const [anime01, setAnime01] = useState(false);
  const [anime02, setAnime02] = useState(false);
  const element = useRef<HTMLDivElement>(null);
  // 是否已经停下来了
  const stoped = useRef(true) 
  useEffect(() => {
    anime01 && animate01();
    anime02 && animate02();
  }, [anime01, anime02]);
  function animate01() {
    anime({
      targets: element.current,
      translateX: 400,
      backgroundColor: '#FF8F42',
      borderRadius: ['0%', '50%'],
      complete: () => {
        setAnime01(false)
        setAnime02(true)
      }
    })
  }
  function animate02() {
    anime({
      targets: element.current,
      translateX: 0,
      backgroundColor: '#FFF',
      borderRadius: ['50%', '0%'],
      easing: 'easeInOutQuad',
      complete: () => {
        setAnime02(false);
        stoped.current = true
      }
    })
  }
  function clickHandler() {
    if (stoped.current) {
      stoped.current = false
      setAnime01(true);
    }
  }
  return (
    <div className="container" onClick={clickHandler}>
      <div className="el" ref={element} />
      {anime01 && <div>第一段动画执行中</div>}
      {anime02 && <div>第二段动画执行中</div>}
    </div>
  )
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.
  • 33.
  • 34.
  • 35.
  • 36.
  • 37.
  • 38.
  • 39.
  • 40.
  • 41.
  • 42.
  • 43.
  • 44.
  • 45.
  • 46.
  • 47.
  • 48.
  • 49.
  • 50.
  • 51.
  • 52.
  • 53.

这个案例值得我们进一步思考,一方面是数据为什么需要使用 state 或者 ref. 另一方面是关于 effect 是否还有另外一个角度的思考。