前言
大家好,本文将详细介绍react中的重要知识点-- useEffect
的核心概念、使用场景和最佳实践,特别适合刚接触 React Hooks 的开发者。
React Hooks 编程基础
在深入 useEffect
之前,让我们先了解一些 React Hooks 的基础知识,下面从大家可能最熟悉的useState开始说起。
useState 简介
useState
是 React 最基础的 Hook,用于在函数组件中添加状态管理。它接收一个初始值,返回一个包含当前状态和更新函数的数组,它让函数组件拥有了状态管理的能力:
const [count, setCount] = useState(0);
以 use
开头的函数:这是 React Hooks 的命名约定,表明这是一个 Hook
在react中,有以下常见hook:
- useState
用于在函数组件中添加状态管理
const [state, setState] = useState(initialState);
- useEffect
处理副作用操作(数据获取、订阅、手动DOM操作等)
useEffect(() => { /* effect */ }, [dependencies]);
- useContext
接收一个 context 对象并返回该 context 的当前值
const value = useContext(MyContext);
- useReducer
更复杂的状态逻辑管理,类似于 Redux 的 reducer
const [state, dispatch] = useReducer(reducer, initialState);
- useCallback
缓存回调函数,避免不必要的重新创建
const memoizedCallback = useCallback(() => { /* 函数 */ }, [deps]);
useEffect是什么?
useEffect
是 React 中最核心的 Hook 之一,用于在函数组件中执行副作用操作。它相当于类组件中的 componentDidMount
、componentDidUpdate
和 componentWillUnmount
的结合体,但更加灵活和强大。
什么是副作用?
在编程中,副作用(Side Effect) 指的是函数或表达式在执行时,对外部环境(如全局变量、DOM、网络请求、文件系统等)产生的影响。简单来说,就是除了返回计算结果之外,还做了其他事情。
React 中的副作用
在 React 组件中,副作用通常指那些不直接参与 UI 渲染,但会影响外部环境的操作,例如:
- 数据获取(API 请求) (如
fetch
、axios
) - 手动修改 DOM(如
document.title
、window.scrollTo
) - 订阅事件(如
addEventListener
、WebSocket
连接) - 设置定时器(如
setTimeout
、setInterval
) - 存储数据(如
localStorage
、IndexedDB
)
副作用的分类
类型 | 例子 | 是否属于副作用? |
---|---|---|
纯函数 | const sum = (a, b) => a + b; | ❌ 无副作用 |
副作用操作 | console.log() 、fetch() 、setTimeout() | ✅ 有副作用 |
React 状态更新 | setState() | ✅ 有副作用(影响组件渲染) |
为什么需要 useEffect
?
React 的核心工作是根据状态(state)渲染 UI,而副作用(如数据请求)可能会影响渲染结果或性能。因此,React 提供了 useEffect
,让开发者可以:
- 在合适的时机(如组件渲染后)执行副作用,避免阻塞 UI 更新。
- 控制副作用的执行频率(通过依赖项数组
[]
)。 - 清理副作用(如取消请求、移除事件监听),防止内存泄漏。
所以总结如下:
- 副作用 = 影响外部世界的操作(如 API 请求、DOM 操作、定时器等)。
useEffect
是 React 管理副作用的 Hook,用于在组件生命周期中安全地执行和清理这些操作。- 纯函数(无副作用)是理想情况,但现实开发中,副作用是不可避免的,关键是如何合理控制它们。
如何使用useEffect呢?
基本语法结构
useEffect(() => {
// 副作用逻辑代码
return () => {
// 清理函数(可选)
};
}, [dependencies]); // 依赖数组(可选)
四种常见使用模式
1.每次渲染后都执行
useEffect(() => {
console.log('组件更新后执行');
});
// 注意:没有依赖数组参数
2.仅在挂载时执行一次
useEffect(() => {
console.log('仅在组件挂载时执行');
return () => {
console.log('组件卸载时执行');
};
}, []); // 空依赖数组
3.特定状态变化时执行
const [count, setCount] = useState(0);
useEffect(() => {
console.log(`count值变为:${count}`);
}, [count]); // 仅在count变化时执行
4.清理副作用示例
useEffect(() => {
const timer = setInterval(() => {
console.log('定时器运行中...');
}, 1000);
return () => {
clearInterval(timer); // 组件卸载时清除定时器
};
}, []);
Effect的一个demo
下面,给出一个完整的例子的代码来说明effect的使用
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;
让我们来对这个demo进行详细地解析:
注意:我们需要先关闭严格模式,打开src中的main.jsx
去除<StrictMode>
// 去除StrictMode 严格模式
//<StrictMode>
<App />
//</StrictMode>,
首先分析初次渲染时
当组件首次加载时,所有 useEffect 钩子都会按照声明的顺序依次执行:
-
模式1:立即执行,因为没有依赖数组,控制台输出"【模式1】组件渲染完成或更新后执行"
-
模式2:立即执行,因为是空依赖数组且首次渲染,控制台输出"【模式2】组件挂载时执行 (仅一次)"
-
模式3:立即执行,因为 count 的初始值(0)被视为"变化",控制台输出"【模式3】count值变化时执行,当前count: 0",同时更新文档标题
-
模式4:立即执行,因为是空依赖数组且首次渲染:
- 控制台输出"【模式4】设置窗口大小监听器"
- 添加窗口 resize 事件监听器
当我们点击增加count时
当用户点击"增加count"按钮时:
-
状态更新:setCount 触发状态更新,count 值增加1
-
重新渲染:组件使用新的 count 值重新渲染
-
useEffect 执行:
-
模式1:再次执行(每次渲染都执行),控制台输出
-
模式2:不执行(依赖数组为空且不是首次渲染)
-
模式3:
- 先执行上一次 effect 的清理函数(如果有),输出"【模式3】count即将变化,或组件卸载时执行"
- 然后执行新的 effect,输出"【模式3】count值变化时执行,当前count: [新值]"
- 更新文档标题
-
模式4:不执行(依赖数组为空且不是首次渲染)
-
当我们缩放页面时
当用户调整浏览器窗口大小时:
-
事件触发:浏览器触发 resize 事件
-
事件处理:模式4中注册的 handleResize 函数被执行:
- 更新 windowWidth 状态
- 控制台输出"窗口大小变化: [新宽度]"
-
状态更新:setWindowWidth 触发状态更新
-
重新渲染:组件使用新的 windowWidth 值重新渲染
-
useEffect 执行:
- 模式1:再次执行(每次渲染都执行),控制台输出
- 其他模式不执行(因为它们的依赖项没有变化)
组件卸载时的执行情况
虽然示例中没有直接展示卸载过程,但了解组件卸载时的行为很重要:
-
执行所有清理函数:
- 模式2:执行清理函数,输出"【模式2】组件卸载时执行"
- 模式4:执行清理函数,输出"【模式4】移除窗口大小监听器",并实际移除事件监听器
-
其他模式:
- 模式1没有清理函数,不执行任何操作
- 模式3如果count在上次渲染后有变化,也会执行其清理函数
总结
这篇文章较新手向,主要介绍了以下内容:
-
useEffect 的本质
React 提供的用于管理副作用的 Hook,统一了类组件中的componentDidMount
、componentDidUpdate
和componentWillUnmount
生命周期方法。 -
副作用的定义
任何影响组件外部世界或与外部系统交互的操作,包括数据获取、订阅、定时器、手动 DOM 操作等。 -
四种核心模式
- 每次渲染后执行(无依赖数组)
- 仅挂载时执行(空依赖数组
[]
) - 特定状态变化时执行(指定依赖项
[state]
) - 需要清理的副作用(返回清理函数)