# useEffect 是?

在 Vue 中,每个组件的生命周期钩子函数是比较重要的。我们经常会在组件挂载和卸载的时候进行一些初始化或者卸载的操作。与此同时,为了监听某项响应式数据的变化并触发相应的函数,我们会使用 watch 这个函数来进行监听。

而在 React 当中,有一个把生命周期钩子函数和监听数据的功能全部合为一体的 Hook,那就是 useEffect

useEffect 是 React 中的一个 Hook,它允许你在函数组件中执行副作用操作。结合 React 组件的生命周期方法,可以把 useEffect 看作是 componentDidMountcomponentDidUpdatecomponentWillUnmount 这些生命周期方法的组合。

使用 useEffect 可以在组件渲染到屏幕之后执行某些操作,比如数据获取、订阅或者手动更改 DOM 等。它接受两个参数:一个是包含副作用逻辑的函数,另一个是一个依赖项数组。依赖项数组是可选的,它告诉 React 只有在依赖项发生变化时才重新执行副作用函数。

# “副作用” 是?

“副作用” 一词,实际上是由函数式编程中的概念演变而来的。在函数式编程中,一个函数的输出只依赖于输入,不会对外部环境产生任何影响,这种函数被称为 纯函数 。而与之相对的,如果一个函数的执行会对外部环境产生影响,比如修改全局变量、修改 DOM 等,那么这种函数就被称为 有副作用 的函数。

# 副作用的定义

一个函数有副作用,如果它在执行时除了返回一个值之外,还做了以下任何事情:

  1. 修改了函数外部的变量或数据结构(例如,修改全局变量或对象属性)。
  2. 进行了输入或输出操作(例如,读写文件、网络请求、打印日志)。
  3. 触发了异常或错误。
  4. 修改了参数(例如,传入对象引用并修改对象属性)。

这些行为会影响程序的其他部分,使得函数的行为变得不可预测和难以测试。

# 纯函数与副作用

在函数式编程中,提倡使用 “纯函数” 来避免副作用。纯函数具有以下两个特征:

  1. 确定性:相同的输入总是返回相同的输出。
  2. 无副作用:函数的执行不影响外部状态,只依赖输入参数,并且不改变外部状态。

# 副作用的例子

# 有副作用的函数

let count = 0;
function increment() {
  count += 1; // 修改了外部变量 count,这是一个副作用
  console.log(count); // 输出操作,这是一个副作用
}
increment(); //count 变为 1,并且在控制台打印 1
increment(); //count 变为 2,并且在控制台打印 2

# 纯函数

function add(a: number, b: number): number {
  return a + b; // 没有副作用,只是返回两个数的和
}
console.log(add(1, 2)); // 打印 3,函数本身没有副作用
console.log(add(1, 2)); // 打印 3,函数本身没有副作用

虽然副作用在编程中是不可避免的,但在函数式编程中,通常会尽量将副作用控制在一定范围内。例如:

  1. 隔离副作用:将副作用封装在特定的函数或模块中,避免它们扩散到整个程序。
  2. 不可变数据:使用不可变的数据结构,避免数据在不同函数之间共享和修改。
  3. 使用纯函数:尽量使用纯函数来构建程序逻辑,将副作用减少到最小。

# React 中的副作用处理

在现代化的 React 组件编写中,官方明确提倡使用函数式组件,而不是类组件。函数式组件的一个重要特性是 纯函数 ,也就是说,在 React 当中,一个组件本身的目的 仅仅只是渲染 。它们接收状态或者 props 的数据,然后将其渲染到对应的位置,而其他关于这个数据的由来、处理等等和渲染本身无关的操作,就可以归类为 副作用

但是我们都知道,一个组件的生命周期中,除了渲染,还有很多其他的操作,比如数据获取、订阅、定时器等等。这些操作都是属于 副作用 的范畴。而 React 为了让我们更好的处理这些副作用,提供了 useEffect 这个 Hook。React 中所谓的 “副作用”(Side Effects)是指那些 影响其他组件或者与外部世界交互的操作 。它们在纯函数组件的主体中无法执行,因为纯函数组件的设计初衷是接受 props 和 state 作为输入,仅仅返回需要渲染的 UI,不应包含任何副作用。

副作用包括但不限于以下几类操作:

  1. 数据获取:例如,从 API 获取数据。这是副作用的一个典型例子,因为你在组件中进行了外部世界的数据交互。
  2. 订阅:例如,WebSocket 或其他推送通知的订阅。订阅外部数据源需要在组件外部建立连接,这也是一个副作用。
  3. 手动修改 DOM:React 通常会管理 DOM,但如果你直接修改 DOM(比如,通过 document.getElementById 等方式),那么这种操作就是副作用。
  4. 定时器:例如,使用 setTimeoutsetInterval 。定时器的设置和清理操作都是副作用,因为它们超出了组件渲染流程的范畴。

# useEffect 的基础使用

它接收两个参数:1. 副作用函数;2. 依赖项。

# 依赖项与副作用函数的执行时机

用户传入的副作用函数的执行时机,是和这第二个参数依赖项相关的。总共无外乎三种情况:

  1. 不传递依赖项(或传递一个空数组)useEffect 的副作用函数会在每次渲染后运行,类似于 componentDidMountcomponentDidUpdate 的结合。
  2. 传递一个空数组 :副作用函数只会在组件挂载时运行一次,类似于 componentDidMount
  3. 传递具体的依赖项 :只有当依赖项发生变化时,副作用函数才会执行,这让你能够优化性能,避免不必要的更新。

以下是一个具体的示例:

//useEffect 的基本使用
//useEffect 中回调函数(副作用函数)的执行时机和传入的依赖项有关
import { useEffect } from "react";
import { useState } from "react";
function Component() {
  const [count, setCount] = useState(0);
  const [name, setName] = useState("张三");
  /* // 1. 没有依赖项,那么副作用函数会在组件初始渲染 + 更新渲染时执行
  useEffect (() => {
    console.log ("useEffect 被触发了");
  }); */
  /* // 2. 空数组依赖,那么副作用函数会在组件初始渲染时执行
  useEffect (() => {
    console.log ("useEffect 被触发了");
  }, []); */
  // 3. 有依赖项,那么副作用函数会在组件初始渲染 + 依赖项更新时执行
  useEffect(() => {
    console.log("useEffect被触发了");
  }, [name]);
  return (
    <div>
      <h1>useEffect的基本使用</h1>
      <p>count: {count}</p>
      <p>name: {name}</p>
      <button
        onClick={() => {
          setCount(count + 1);
        }}
      >
        点我进行数据的累加
      </button>
      <input
        type="text"
        onChange={(e) => {
          setName(e.target.value);
        }}
      />
    </div>
  );
}
export default Component;

这个例子其实没什么好说的,要说的就是第一个例子:没有依赖项的时候,只在 组件更新渲染的时候触发 。什么时候会让这个组件更新渲染呢?在 React 中最频繁的例子就是 state(状态)的改变,在组件中使用这个状态的位置便会重新渲染以响应变化。

# 关于副作用函数的一些限制

首先, useEffect 这个 hook 不需要用变量去接收它的返回值 ,因为这个 hook 期望的返回是一个清理函数或者没有返回值( undefined )。

这也意味着,这个 hook 是不能被 async 进行修饰的,因为 async 修饰的函数默认返回的是一个 Promise。 所以,当遇到一些需要采用异步方式进行的操作,比如调用 api 获取数据,这就需要额外在回调函数体内部声明获取数据的异步函数并且直接执行。

import React, { useState, useEffect } from "react";
function Example() {
  const [data, setData] = useState(null);
  useEffect(() => {
    // 在 useEffect 内部定义一个异步函数
    const fetchData = async () => {
      const response = await fetch("https://api.example.com/data");
      const result = await response.json();
      setData(result);
    };
    // 调用该异步函数
    fetchData();
  }, []); // 依赖数组为空,表示这个 effect 仅在组件挂载完成后运行一次
  // 渲染数据...
}

这个是我踩了很多坑过来的,因为一般的获取数据全都放在 useEffect 这个 hook 里面的。。。

# useEffect 清除副作用

上面也说了,useEffect 期望啥都不返回或者返回一个 清理函数 。这个清理函数就是用来清除在副作用函数中声明的副作用的。

useEffect(() => {
  const timerId = setInterval(() => {
    setTimeLeft((prevTime) => prevTime - 1);
  }, 1000);
  return () => {
    clearInterval(timerId); // 组件卸载时清理定时器
  };
}, []); // 空依赖数组表示仅在组件挂载时执行

比如这个例子,先在函数体创建了一个定时器,名为 timerId ,然后我们想要在这个组件卸载时候把这个定时器清理掉,否则会造成内存泄漏之类的。 写在 useEffect 清理函数里面的,都会在其卸载的生命周期钩子( componentWillUnmount )里面执行掉。 这样之后,在卸载时候就会把这个计时器给及时的清除掉。

# 总结

React 这个 hook 的用法可以说是把很多不用的生命周期全都扔到一边了,并且把监听数据的操作也一并纳入,综其名曰 副作用函数 。这一点我觉得比 Vue 的生命周期 + watch 的方式要优雅方便。实际上,这也能够直接地体现出两种框架编程范式的不同。Vue 是基于响应式编程的,而 React 是基于函数式编程的。所以,React 的 hook 也是更符合函数式编程的思想。