React useEffect:新手友好的全方位指南

372 阅读7分钟

引言

你是否也遇到这些问题?

为什么我的数据请求总是重复发送?

组件卸载后,定时器还在运行,导致报错?

useEffect 的依赖数组到底该怎么写?

如果你在学习 React 时也有类似困惑,别担心,本文将带你一步步搞懂 useEffect!

一、React Hooks 基础回顾

在 React 16.8 之后,函数组件也能拥有状态和副作用管理能力,这都得益于 Hooks。最常用的有:

  • useState:让组件拥有自己的状态
    const [count, setCount] = useState(0);
    
  • useEffect:处理副作用(如数据请求、事件监听、DOM 操作等)
  • useContextuseReduceruseCallback

二、什么是 useEffect?为什么要用它?

1. 副作用的概念

副作用(Side Effect)指的是函数执行时对外部环境产生影响的操作,比如:

  1. 发送网络请求
  2. 操作 DOM
  3. 订阅/解绑事件
  4. 启动/清除定时器

这些操作不直接影响组件的 UI,但会影响外部世界。

2. useEffect 的作用

useEffect 是 React 官方推荐的副作用管理方案。它让你可以:

  1. 在合适的时机执行副作用,避免阻塞 UI
  2. 精确控制副作用的执行频率
  3. 清理副作用,防止内存泄漏

三、useEffect 的基本用法与四大模式

基本语法

useEffect(() => {
  // 副作用逻辑
  return () => {
    // 清理逻辑(可选)
  };
}, [依赖项]);

1. 最基础:每次渲染后都执行

场景说明:
这是 useEffect 最简单的用法。无论组件是首次渲染还是后续更新,副作用函数都会被执行。常用于调试、日志输出等场景。

代码示例:

useEffect(() => {
  console.log('组件渲染或更新时都会执行');
});

要点:

  • 没有依赖数组,意味着每次渲染(包括初次和每次更新)都会执行。
  • 适合需要追踪所有渲染的场景。

2. 进阶:仅在组件挂载和卸载时执行

场景说明:
有些副作用只需要在组件首次出现时执行一次(如初始化数据、注册全局事件),并在组件销毁时清理(如移除事件监听)。

代码示例:

useEffect(() => {
  console.log('组件挂载时执行');
  return () => {
    console.log('组件卸载时执行');
  };
}, []);

要点:

  • 依赖数组为空,副作用只在组件挂载和卸载时执行一次。
  • 常用于事件监听、定时器初始化等场景。

3. 实用:依赖特定状态变化时执行

场景说明:
当你只想在某个状态(如 count)变化时执行副作用,比如根据用户输入请求数据、更新页面标题等。

代码示例:

const [count, setCount] = useState(0);

useEffect(() => {
  document.title = `当前计数:${count}`;
  console.log(`count 变化为:${count}`);
  return () => {
    console.log('count 即将变化或组件卸载');
  };
}, [count]);

要点:

  • 依赖数组中写入 count,只有 count 变化时才会执行副作用。
  • 返回的清理函数会在 count 变化前或组件卸载时执行。

4. 深入:副作用的清理与资源管理

场景说明:
有些副作用需要在组件卸载或依赖变化时进行清理,比如定时器、事件监听等,否则可能导致内存泄漏或重复注册。

代码示例:

useEffect(() => {
  const handleResize = () => {
    console.log('窗口大小变化');
  };
  window.addEventListener('resize', handleResize);

  return () => {
    window.removeEventListener('resize', handleResize);
    console.log('移除窗口大小监听');
  };
}, []);

要点:

  • 在副作用函数中注册事件监听或定时器。
  • 在返回的清理函数中移除监听或清除定时器,保证资源被正确释放。
  • 依赖数组为空,表示只在挂载和卸载时处理。

综合案例

下面给出一个完整的例子的代码来演示 useEffect 的四种典型用法,并通过不同的状态和交互,帮助你理解副作用的触发时机和清理机制。

import React, { useState, useEffect } from 'react';

function UseEffectExamples() {
  const [count, setCount] = useState(0);
  const [data, setData] = useState('初始数据');
  const [windowWidth, setWindowWidth] = useState(window.innerWidth);

  // 模式1: 每次渲染后都执行
  useEffect(() => {
    console.log('【模式1】组件渲染完成或更新后执行');
  }); // 没有依赖数组

  // 模式2: 仅在挂载时执行一次
  useEffect(() => {
    console.log('【模式2】组件挂载时执行 (仅一次)');
    
    return () => {
      console.log('【模式2】组件卸载时执行');
    };
  }, []); // 空依赖数组

  // 模式3: 特定状态变化时执行
  useEffect(() => {
    console.log(`【模式3】count值变化时执行,当前count: ${count}`);
    
    // 这里可以添加count变化时需要执行的逻辑
    document.title = `当前计数: ${count}`;
    
    return () => {
      console.log('【模式3】count即将变化,或组件卸载时执行');
    };
  }, [count]); // 依赖count

  // 模式4: 清理副作用的示例 (窗口大小监听)
  useEffect(() => {
    console.log('【模式4】设置窗口大小监听器');
    
    const handleResize = () => {
      setWindowWidth(window.innerWidth);
      console.log('窗口大小变化:', window.innerWidth);
    };
    
    window.addEventListener('resize', handleResize);
    
    return () => {
      console.log('【模式4】移除窗口大小监听器');
      window.removeEventListener('resize', handleResize);
    };
  }, []); // 空依赖数组,只设置一次

  return (
    <div style={{ padding: '20px', border: '1px solid #ccc', margin: '20px' }}>
      <h2>useEffect 四种使用模式示例</h2>
      
      <div style={{ marginBottom: '20px' }}>
        <h3>模式1: 每次渲染后都执行</h3>
        <p>查看控制台输出</p>
      </div>
      
      <div style={{ marginBottom: '20px' }}>
        <h3>模式2: 仅挂载时执行一次</h3>
        <p>查看控制台初始化和卸载时的输出</p>
      </div>
      
      <div style={{ marginBottom: '20px' }}>
        <h3>模式3: count变化时执行 (当前count: {count})</h3>
        <button onClick={() => setCount(c => c + 1)}>增加count</button>
        <p>查看控制台和页面标题的变化</p>
      </div>
      
      <div style={{ marginBottom: '20px' }}>
        <h3>模式4: 清理副作用示例</h3>
        <p>当前窗口宽度: {windowWidth}px</p>
        <p>尝试改变浏览器窗口大小,查看控制台输出</p>
      </div>
      
      <div>
        <h3>数据变化示例</h3>
        <input 
          value={data}
          onChange={(e) => setData(e.target.value)}
          placeholder="输入数据观察模式1的执行"
        />
      </div>
    </div>
  );
}

export default UseEffectExamples;

首先,组件通过 useState 定义了三个状态变量:

  • count:用于计数,初始值为 0。
  • data:用于保存输入框的内容,初始值为“初始数据”。
  • windowWidth:用于记录当前窗口的宽度,初始值为 window.innerWidth

这些状态分别用于演示不同的副作用场景。

然后,组件内部依次设置了四个 useEffect,分别对应四种常见的副作用处理模式:

  1. 首先,第一个 useEffect 没有依赖数组,因此每次组件渲染(包括初次和每次更新)都会执行。它在控制台输出“【模式1】组件渲染完成或更新后执行”,用于演示无依赖时副作用的触发时机。

  2. 接着,第二个 useEffect 的依赖数组为空([]),所以只会在组件挂载时执行一次,并在组件卸载时执行返回的清理函数。它分别在挂载和卸载时输出“【模式2】组件挂载时执行 (仅一次)”和“【模式2】组件卸载时执行”。这种模式常用于只需初始化一次或全局事件注册的场景。

  3. 其次,第三个 useEffect 依赖于 count只有当 count 发生变化时才会执行。它会在控制台输出当前 count 的值,并同步更新页面标题为“当前计数: [count]”。此外,每次 count 变化前或组件卸载时,还会执行清理函数,输出“【模式3】count即将变化,或组件卸载时执行”。这种模式适合响应特定状态变化的副作用。

  4. 最后,第四个 useEffect 主要用于注册和清理全局事件监听。它在组件挂载时为窗口注册 resize 事件监听器,每当窗口大小变化时,更新 windowWidth 并输出新宽度。组件卸载时,清理该事件监听器,防止内存泄漏。相关输出分别为“【模式4】设置窗口大小监听器”和“【模式4】移除窗口大小监听器”。


就这样,组件的渲染部分将这四种模式的效果分别展示出来:

  • 通过不同的 <div> 区块,分别介绍每种模式的作用和观察方式。
  • “增加count”按钮用于触发 count 的变化,便于观察模式3的效果。
  • 输入框用于修改 data,每次输入都会导致组件重新渲染,从而触发模式1。
  • 当前窗口宽度的显示和窗口大小变化的监听,直观展示了模式4的应用。

总结

文章结构为:

  1. 明确副作用的本质
    副作用是指那些会影响组件外部世界的操作,如数据请求、事件监听、定时器等。React 推荐用 useEffect 统一管理这些副作用,保证组件行为的可控性和健壮性。

  2. 掌握 useEffect 的基本语法与四大模式

    • 每次渲染后都执行(无依赖数组)
    • 仅挂载和卸载时执行(空依赖数组)
    • 依赖特定状态变化时执行(依赖数组含状态)
    • 清理副作用,防止内存泄漏(返回清理函数)
  3. 通过综合案例加深理解
    通过一个完整的代码示例,直观展示了 useEffect 在不同场景下的用法和触发时机。你可以通过按钮点击、输入内容、调整窗口大小等操作,亲自体验副作用的执行与清理过程。


`