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)} />;
};
相关推荐
光影少年14 小时前
react的Context 跨层传值、优缺点、适用场景
前端·react.js·掘金·金石计划
JiaWen技术圈16 小时前
React Server Functions 深度解析
前端·react.js·前端框架
JiaWen技术圈17 小时前
React 19 并发渲染器:全面解析与实战指南
前端·react.js·前端框架
Ruihong19 小时前
VuReact v1.8.4 发布:Vue 迁移 React 编译器迎来稳定性大修,这些坑终于被填平了
前端·vue.js·react.js
从文处安19 小时前
「React Router v7 教程」从零到全栈,一篇搞定
前端·react.js
卸任19 小时前
打造基于 Milkdown 的所见即所得 Markdown 编辑器
前端·react.js·markdown
JiaWen技术圈20 小时前
React 19 Fiber 架构 深度解析
前端·react.js·架构
暗冰ཏོ20 小时前
《Vue + React + Java + PHP 项目部署到服务器完整指南》
java·服务器·vue.js·react.js·项目部署
JeariCk20 小时前
React Compiler 1.0 发布半年后的现状
react.js
. . . . .20 小时前
React Native
react native·react.js