Hooks-useEffect

前置知识

1. 为什么我们需要学 useEffect?

在 React 的世界里,我们总是追求组件的纯粹性 ------给定相同的 props,永远返回相同的 UI。但现实往往是骨感的,我们需要处理数据获取、订阅消息、手动修改 DOM 等"脏活累活"。

在编程中,这些与外部世界交互、产生额外影响 的操作,就被称为副作用

为了协调纯粹的 UI 渲染与复杂的现实需求 ,React 提供了 useEffect Hook。在深入它之前,我们需要先厘清两个核心概念。

2. 保持组件纯粹性与副作用

问题:什么是副作用函数,什么是纯函数?(面试题)

纯函数定义:

  1. 相同的输入永远会得到相同的输出。这意味着函数的行为是可预测的。
  2. 只负责自己的任务,无副作用出现:它只关心自己的内部逻辑(第一点),绝不会去修改函数外部的任何状态(比如全局变量、DOM、文件系统等)。
typescript 复制代码
// 纯函数:只负责计算,不依赖也不修改外部状态
const sum = (x: number, y: number): number => x + y;
​
sum(1, 2); // 始终返回 3

副作用函数定义

一个函数在执行过程中,除了返回值 之外,还对外部环境产生了可观察的影响修改,这个函数就是副作用函数。

常见的副作用行为包括:

  1. 修改外部变量:修改全局变量、修改传入的参数对象(引用类型)。
  2. I/O 操作:发起网络请求(AJAX/Fetch)、读写文件、读写数据库。
  3. DOM 操作:修改网页标题、手动更改 DOM 节点结构。
  4. 系统交互 :设置定时器(setTimeout)、订阅事件(addEventListener)、打印日志(console.log)。
  5. 非确定性 :依赖随机数(Math.random)或当前时间(new Date),导致相同输入得到不同输出。
typescript 复制代码
let total: number = 0; // 外部变量
​
// 副作用函数:修改了外部的 'total' 变量
function apple(value: number): number {
  total += value;
  return total;
}

3. React 组件应该是纯函数

React 的核心设计理念之一就是:组件是一个纯函数

核心原则

组件的职责非常单一------根据 propsstate 计算并返回 JSX。在渲染阶段,组件不应包含任何副作用。

注意

这里说的"不能有副作用",是指渲染过程中不能有。渲染阶段只管教一件事------根据 props 和 state 算出 UI。

javascript 复制代码
// 纯组件:相同的 props 总是渲染出相同的结果
function Greeting({ name }) {
  return <h1>你好,欢迎{name}学习React!</h1>;
}

React 依赖这个纯粹的约定来实现很多优化(比如跳过不必要的重新渲染),如果组件在渲染过程中偷偷做了额外的事情,就会打破这个约定,导致难以追踪的 bug。

javascript 复制代码
// 错误示范:在渲染过程中执行副作用
function UserProfile({ userId }) {
  // 每次渲染都会发起请求,而且无法去控制时机和清理
  fetch(`/api/users/${userId}`).then(...)
  return <div>用户资料</div>;
}

为什么这样做是危险的?

如果在渲染阶段直接写副作用,会引发以下严重后果:

  1. 请求失控(发多少次你说了不算)

    • React 可能会因为并发模式或父组件更新而多次调用组件函数。如果在函数体内直接请求,会导致重复请求,浪费资源。
  2. 内存泄漏(关不掉)

    • 如果在渲染时开启了定时器或订阅,当组件被卸载时,这些任务可能仍在后台运行。这不仅浪费性能,还可能试图更新一个不存在的组件状态,导致报错。
  3. 缺乏控制力

    • 渲染的时机由 React 决定。将副作用放在渲染主体中,意味着你无法控制它何时执行执行几次 以及依赖什么条件

React 组件应当保持纯粹,只负责 UI 的映射。而处理副作用(如请求数据、定时器)的任务,需要交给专门的机制------也就是我们接下来要学习的 useEffect

4. useEffect 基本用法

scss 复制代码
useEffect(setup, dependencies?)

参数

  • setup(必选) :Effect处理函数,可以返回一个清理函数(cleanup)。组件挂载时执行setup,依赖项更新时先执行cleanup再执行setup,组件卸载时执行cleanup。
  • dependencies(可选) :setup中使用到的响应式值列表(props、state等)。必须以数组形式编写如[dep1, dep2]。不传则每次重渲染都执行Effect。

React 内部是用 Object.is(类似 ===)来对比依赖项有没有变化的。

scss 复制代码
useEffect(() => {
  // 副作用代码
}, [/* 依赖项数组 */]);

返回值

useEffect 返回undefined

javascript 复制代码
let a = useEffect(() => {})
console.log('a', a) //undefined

使用步骤:

先从 React 中导入 useEffect Hook

javascript 复制代码
import { useEffect } from 'react';

再在组件顶部调用, 并在其中加入一些代码:(不要在循环或条件判断中调用 Hook。)

javascript 复制代码
function MyComponent() {
  useEffect(() => {
    // 每次渲染后都会执行此处的代码
    // 这里就是你的"副作用"发生的地方
  });
​
  return <div>我的组件</div>;
}

每当你的组件渲染时,React 会先更新页面,然后再运行 useEffect 中的代码。换句话说,useEffect 会"延迟"一段代码的运行,直到渲染结果反映在页面上 。它是异步执行的,不会阻塞浏览器绘制屏幕

执行时机:什么是"渲染后"?

理解 useEffect 的核心在于理解它的执行时机

React 的工作流程是这样的:

  1. 渲染阶段:React 调用你的组件函数,计算出需要更新的 JSX(虚拟 DOM)。
  2. 提交阶段:React 将变更应用到真实的 DOM 上,屏幕上的画面更新了。
  3. 副作用阶段 :React 执行你在 useEffect 中定义的代码。

核心概念useEffect 会"延迟"一段代码的运行,直到渲染结果已经反映在页面上

这意味着,如果你的副作用代码需要获取 DOM 节点的尺寸、位置,或者需要触发一个不阻塞浏览器绘制的网络请求,useEffect 是完美的选择。

4. useEffect 的三种执行模式

useEffect 的行为完全取决于第二个参数(依赖项数组)

1. 不传参数:每次渲染都执行

  • 特点:没有任何限制,组件只要更新(无论是哪个 state 变了),它就会跑。
  • 类比componentDidMount + componentDidUpdate
javascript 复制代码
import { useEffect, useState } from "react";
​
const App = () => {
  const [count, setCount] = useState(0);
  const [text, setText] = useState("");
​
  // 没有第二个参数:只要组件重新渲染(count 或 text 变动),这里都会执行
  useEffect(() => {
    console.log("只要渲染,我就执行");
  });
​
  return (
    <div>
      <button onClick={() => setCount(count + 1)}>Count: {count}</button>
      <input value={text} onChange={(e) => setText(e.target.value)} />
    </div>
  );
};

2. 空数组 []:只在挂载时执行一次

  • 特点:相当于"初始化"操作,只跑一次,后面不管怎么更新都不管了。
  • 类比componentDidMount
javascript 复制代码
import { useEffect, useState } from "react";
​
const App = () => {
  const [count, setCount] = useState(0);
​
  // 空数组:只有组件第一次显示在页面上时执行,后续 count 变化不会触发
  useEffect(() => {
    console.log("仅执行一次(适合初始化、请求接口)");
  }, []);
​
  return <button onClick={() => setCount(count + 1)}>{count}</button>;
};

3. 有依赖项 [value]:数据变了才执行

  • 特点:精确监听。只有数组里的变量发生变化,才会触发。
  • 类比 :特定的 componentDidUpdate
javascript 复制代码
import { useEffect, useState } from "react";
​
const App = () => {
  const [count, setCount] = useState(0);
  const [text, setText] = useState("");
​
  // 依赖 count:只有 count 变化时才执行。text 变化不会触发。
  useEffect(() => {
    console.log("只有 count 变了,我才执行");
  }, [count]);
​
  return (
    <div>
      <button onClick={() => setCount(count + 1)}>Count: {count}</button>
      <input value={text} onChange={(e) => setText(e.target.value)} />
    </div>
  );
};

清理函数:善后工作

副作用往往需要"打扫战场"(比如清除定时器、移除监听)。useEffect 允许你返回一个函数,这就是清理函数。

  • 执行时机:

    组件卸载时。

    依赖项变化,下一次 Effect 执行前(先清理旧的,再执行新的)。

场景:定时器清理

javascript 复制代码
import { useEffect, useState } from "react";
​
const App = () => {
  const [count, setCount] = useState(0);
  useEffect(() => {
    // 1. 创建副作用(开启定时器)
    const timer = setInterval(() => {
      setCount((c) => c + 1);
    }, 1000);
​
    // 2. 返回清理函数(清除定时器)
    // 当组件卸载 或 依赖变化重跑前,React 会自动调用这个函数
    return () => {
      clearInterval(timer);
      console.log("定时器已清理");
    };
  }, []); // 空数组保证只开启一次定时器
​
  return <div>计时:{count}</div>;
};

场景:解决"请求竞态"问题

当用户快速输入时,我们希望取消上一次还没发完的请求,只保留最后一次的请求。

javascript 复制代码
import { useEffect, useState } from "react";
​
const Search = () => {
  const [keyword, setKeyword] = useState("");
​
  useEffect(() => {
    // 1. 模拟开启一个定时器(代表网络请求)
    const timer = setTimeout(() => {
      console.log(`发送请求:${keyword}`);
    }, 500);
​
    // 2. 清理函数:如果用户又输入了新字,这里会先执行,清除上一次的定时器
    return () => {
      clearTimeout(timer);
      console.log("取消上一次过期的请求");
    };
    
  }, [keyword]); // 依赖 keyword,每次输入变化都会触发
​
  return <input value={keyword} onChange={(e) => setKeyword(e.target.value)} />;
};
相关推荐
光影少年1 小时前
react函数组件、类组件、纯组件、受控/非受控组件
前端·react.js·掘金·金石计划
空中海3 小时前
05 React Native架构设计、主线项目与专家实践
javascript·react native·react.js
killerbasd13 小时前
还是迷茫 5.3
前端·react.js·前端框架
江南十四行19 小时前
ReAct Agent 基本理论与项目实战(一)
前端·react.js·前端框架
谢尔登1 天前
10_从 React Hooks 本质看 useState
前端·ubuntu·react.js
辰同学ovo1 天前
从全局登录状态管理学习 Redux
前端·javascript·学习·react.js
光影少年1 天前
reeact虚拟DOM、Diff算法原理、key的作用与为什么不能用index
前端·react.js·掘金·金石计划
江南十四行1 天前
ReAct Agent 基本理论与项目实战(二)
前端·react.js·前端框架
摘星编程1 天前
当AI开始学会“使用工具“——从ReAct到MCP,大模型如何获得真正的行动力
前端·人工智能·react.js