前言
在 React 的函数式组件开发中,useEffect
是一个极其重要的 Hook。它让我们能够在函数组件中处理副作用操作,如数据获取、订阅、手动 DOM 操作等。
React Hooks 基础回顾
在深入 useEffect
之前,我们先简单回顾一下 React Hooks 的基本概念。Hooks 是 React 16.8 引入的新特性,允许我们在不编写 class 组件的情况下使用 state 和其他 React 特性。
而useEffect
是 Hooks 中最核心的一个,用于处理副作用操作。
useEffect深度解析
类型 | 示例 | 是否属于副作用 |
---|---|---|
纯函数 | const sum = (a, b) => a + b |
❌ 无副作用 |
副作用操作 | fetch() , setTimeout() |
✅ 有副作用 |
状态更新 | setState() |
✅ 有副作用 |
基本概念
useEffect
是React专门用于处理副作用的Hook,它能够:
- 在组件渲染后执行副作用操作
- 控制副作用的执行时机
- 提供清理机制避免内存泄漏
副作用类型分析
执行机制详解
useEffect
的执行可以分为三个阶段:
-
挂载阶段
- 组件首次渲染后执行
- 类似于类组件的
componentDidMount
-
更新阶段
- 依赖项变化时重新执行
- 类似于类组件的
componentDidUpdate
-
卸载阶段
- 组件卸载时执行清理函数
- 类似于类组件的
componentWillUnmount
实战应用模式
模式一:持续执行的副作用
当不需要控制执行时机时,可以省略依赖数组:
jsx
useEffect(() => {
console.log('每次渲染后都会执行');
});
模式二:单次执行的副作用
通过空依赖数组控制只在挂载时执行:
jsx
useEffect(() => {
console.log('仅在组件挂载时执行');
return () => {
console.log('组件卸载时执行清理');
};
}, []);
模式三:条件执行的副作用
通过指定依赖项控制执行时机:
jsx
const [count, setCount] = useState(0);
useEffect(() => {
console.log(`count变为: ${count}`);
document.title = `当前计数: ${count}`;
}, [count]);
模式四:需要清理的副作用
当副作用涉及资源管理时,必须提供清理函数:
jsx
useEffect(() => {
const timer = setInterval(() => {
console.log('定时器运行中');
}, 1000);
return () => {
clearInterval(timer);
};
}, []);
综合例子解析
下面通过一个完整的组件示例,展示useEffect
的四种使用模式:
jsx
import React, { useState, useEffect } from 'react';
function EffectDemo() {
const [count, setCount] = useState(0);
const [data, setData] = useState('初始数据');
const [width, setWidth] = useState(window.innerWidth);
// 模式1: 持续执行
useEffect(() => {
console.log('【模式1】每次渲染后执行');
});
// 模式2: 单次执行
useEffect(() => {
console.log('【模式2】组件挂载时执行');
return () => {
console.log('【模式2】组件卸载时清理');
};
}, []);
// 模式3: 条件执行
useEffect(() => {
console.log(`【模式3】count变为: ${count}`);
document.title = `当前计数: ${count}`;
return () => {
console.log('【模式3】清理前一次effect');
};
}, [count]);
// 模式4: 需要清理
useEffect(() => {
console.log('【模式4】设置窗口监听器');
const handleResize = () => {
setWidth(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' }}>
<h2>useEffect四种模式示例</h2>
{/* 各模式展示区域 */}
</div>
);
}
组件渲染流程分析
让我们分析一下组件在不同情况下的行为:
1. 组件首次挂载
-
执行所有
useEffect
钩子(按照声明顺序)- 模式1: 立即执行,控制台输出"【模式1】每次渲染后执行"
- 模式2: 立即执行,控制台输出"【模式2】组件挂载时执行"
- 模式3: 立即执行(因为count初始值变化被视为"变化"),控制台输出"【模式3】count变为: 0",设置文档标题
- 模式4: 立即执行,控制台输出"【模式4】设置窗口监听器",添加resize事件监听器
2. 用户点击增加count按钮
-
setCount
触发状态更新 -
组件重新渲染
-
执行所有
useEffect
钩子:
-
模式1: 再次执行,控制台输出"【模式1】每次渲染后执行"
-
模式2: 不执行(依赖数组为空且不是首次渲染)
-
模式3:
先执行上一次effect的清理函数(如果有),控制台输出"【模式3】清理前一次effect",
然后执行新的effect,控制台输出"【模式3】count变为: [新值]",更新文档标题。
-
模式4: 不执行(依赖数组为空且不是首次渲染)
3. 调整浏览器窗口大小
-
浏览器触发resize事件
-
模式4中的
handleResize
函数被执行:
- 更新
width
状态 - 控制台输出"窗口大小变化: [新宽度]"
-
状态更新触发组件重新渲染
-
执行所有
useEffect
钩子:
- 模式1: 再次执行,控制台输出"【模式1】每次渲染后执行"
- 模式2: 不执行
- 模式3: 不执行(count没有变化)
- 模式4: 不执行(依赖数组为空且不是首次渲染)
4. 组件卸载
React调用所有effect的清理函数(按照与声明相反的顺序):
- 模式4: 执行清理函数,控制台输出"【模式4】移除窗口监听器"
- 模式3: 执行清理函数,控制台输出"【模式3】清理前一次effect"
- 模式2: 执行清理函数,控制台输出"【模式2】组件卸载时清理"
- 模式1: 没有清理函数,不执行任何操作