第12章 useEffect与生命周期
组件的副作用处理和生命周期管理
作者:zyyc-0336 | 日期:2026-06-12
🗺️ 学习路径建议
1. 先学 12.1(什么是副作用),理解为什么需要 useEffect
2. 再学 12.2(依赖数组的三种情况),这是最关键的部分
3. 最后学 12.3(清理函数),处理事件监听和定时器
⚠️ 常见错误提示(点击展开)
useEffect 的回调函数不能是 async 函数,要 async 就在回调内部定义一个 async 函数再调用
依赖数组写了就不要少写——漏写会导致闭包陷阱(数据不更新)
[] 空数组 ≠ 不传依赖数组。不传 = 每次渲染都执行,[] = 只执行一次useEffect 是在渲染之后执行的,不是渲染之前
12.1 什么是副作用
副作用(Side Effect)就是组件渲染之外的"额外操作"——比如发网络请求、操作 DOM、设置定时器、读写 localStorage 等。
React 组件的核心职责是"返回 JSX"。但有些事情你必须做,又不属于"返回 JSX"的范畴,这些就是副作用。
常见的副作用
| 副作用 | 说明 |
|---|---|
| 网络请求 | 从服务器获取文章数据 |
| 操作 DOM | 设置页面标题、滚动到顶部 |
| 定时器 | 倒计时、自动轮播 |
| 事件监听 | 监听窗口大小变化、键盘事件 |
| 本地存储 | 读写 localStorage |
useEffect 的基本语法
JavaScript
import { useEffect } from 'react';
useEffect(function() {
// 副作用代码(在这里做"额外的事")
}, [依赖数组]);
完整示例:页面标题随点击变化
📄 HTML 完整代码(点击折叠)
HTML + React
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>useEffect 基础示例</title>
</head>
<body>
<div id="root"></div>
<script src="https://unpkg.com/react@18/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom@18/umd/react-dom.development.js"></script>
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
<script type="text/babel">
function App() {
let [count, setCount] = React.useState(0);
// useEffect:每次 count 变化时更新页面标题
React.useEffect(function() {
document.title = `你点击了 ${count} 次`;
}, [count]); // 依赖 count,count 变了才执行
return (
<div style={{ textAlign: 'center', padding: '50px' }}>
<h1>useEffect 基础</h1>
<p style={{ fontSize: '48px' }}>{count}</p>
<button onClick={() => setCount(count + 1)} style={{
padding: '10px 30px',
fontSize: '18px',
cursor: 'pointer'
}}>
点击 +1
</button>
<p style={{ color: '#999', marginTop: '20px' }}>
注意看浏览器标签页的标题在变化 ↑
</p>
</div>
);
}
ReactDOM.createRoot(document.getElementById('root')).render(<App />);
</script>
</body>
</html>
代码逐行讲解
| 代码 | 说明 |
|---|---|
React.useEffect(function() { ... }, [count]) | 注册副作用,依赖 count |
document.title = ... | 副作用操作:修改页面标题 |
[count] | 依赖数组——只有 count 变化时才重新执行 |
12.2 依赖数组三种情况
依赖数组决定 useEffect 什么时候执行。
情况一:不传依赖数组——每次渲染都执行
JavaScript
useEffect(function() {
console.log('每次渲染都会执行');
});
// 没有第二个参数 → 组件每次渲染后都执行
情况二:空数组 []——只在首次渲染执行一次
JavaScript
useEffect(function() {
console.log('只在首次渲染执行一次');
}, []);
// [] 空数组 → 只在组件第一次渲染后执行
情况三:有依赖 [a, b]——依赖变化时执行
JavaScript
useEffect(function() {
console.log('count 或 name 变化时执行');
}, [count, name]);
// [count, name] → 这两个值任何一个变化时才执行
完整示例:依赖数组三种情况对比
📄 HTML 完整代码(点击折叠)
HTML + React
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>依赖数组三种情况</title>
</head>
<body>
<div id="root"></div>
<script src="https://unpkg.com/react@18/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom@18/umd/react-dom.development.js"></script>
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
<script type="text/babel">
function App() {
let [count, setCount] = React.useState(0);
let [name, setName] = React.useState('小明');
// 情况1:每次渲染都执行
React.useEffect(function() {
console.log('【情况1】每次渲染都执行');
});
// 情况2:只执行一次(首次渲染)
React.useEffect(function() {
console.log('【情况2】只执行一次(初始化)');
}, []);
// 情况3:count 变化时执行
React.useEffect(function() {
console.log('【情况3】count 变化了:' + count);
}, [count]);
// 模拟获取文章数据(只在组件首次加载时执行)
React.useEffect(function() {
console.log('模拟获取文章数据...');
// 模拟异步请求
let timer = setTimeout(() => {
console.log('文章数据加载完成');
}, 1000);
// 清理定时器(下一节讲)
return () => clearTimeout(timer);
}, []);
return (
<div style={{ maxWidth: '600px', margin: '0 auto', padding: '20px' }}>
<h1>依赖数组三种情况</h1>
<p>count: {count} | name: {name}</p>
<button onClick={() => setCount(count + 1)}>count +1</button>
<button onClick={() => setName(name === '小明' ? '小红' : '小明')}
style={{ marginLeft: '8px' }}>
切换名字
</button>
<p style={{ color: '#999', marginTop: '20px' }}>
打开控制台查看日志输出
</p>
</div>
);
}
ReactDOM.createRoot(document.getElementById('root')).render(<App />);
</script>
</body>
</html>
依赖数组对照表
| 写法 | 执行时机 | 场景 |
|---|---|---|
| 不传(无第二个参数) | 每次渲染后 | 很少用,容易性能问题 |
[] | 仅首次渲染 | 获取初始数据、添加全局事件 |
[a, b] | a 或 b 变化时 | 根据数据变化做操作 |
⚠️ 新手易错:依赖数组里的值必须和回调里使用的外部变量一致。如果回调里用了
count 但依赖数组写 [],回调里拿到的 count 永远是初始值 0(闭包陷阱)。12.3 清理函数
有些副作用需要"清理"——比如定时器要清除、事件监听要移除。useEffect 的回调函数可以返回一个清理函数。
JavaScript
useEffect(function() {
// 执行副作用(注册)
let timer = setInterval(() => { ... }, 1000);
// 返回清理函数(清除)
return function() {
clearInterval(timer);
};
}, []);
完整示例:文章列表 + 窗口尺寸监听
📄 HTML 完整代码(点击折叠)
HTML + React
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>清理函数示例</title>
</head>
<body>
<div id="root"></div>
<script src="https://unpkg.com/react@18/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom@18/umd/react-dom.development.js"></script>
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
<script type="text/babel">
// ========== 模拟获取文章列表(带 loading 状态) ==========
function PostList() {
let [posts, setPosts] = React.useState([]);
let [loading, setLoading] = React.useState(true);
let [error, setError] = React.useState(null);
React.useEffect(function() {
console.log('开始加载文章...');
// 模拟网络请求
let timer = setTimeout(() => {
let data = [
{ id: 1, title: 'JavaScript 入门' },
{ id: 2, title: 'CSS 布局技巧' },
{ id: 3, title: 'React 快速上手' }
];
setPosts(data);
setLoading(false);
console.log('文章加载完成');
}, 1500);
return () => {
clearTimeout(timer);
console.log('清理定时器');
};
}, []);
if (loading) return <p>⏳ 加载中...</p>;
if (error) return <p>❌ 出错了:{error}</p>;
return (
<div>
<h2>文章列表</h2>
{posts.map(post => (
<div key={post.id} style={{
padding: '12px',
marginBottom: '8px',
border: '1px solid #ddd',
borderRadius: '6px'
}}>
{post.title}
</div>
))}
</div>
);
}
// ========== 窗口尺寸监听 ==========
function WindowSize() {
let [size, setSize] = React.useState({
width: window.innerWidth,
height: window.innerHeight
});
React.useEffect(function() {
function handleResize() {
setSize({
width: window.innerWidth,
height: window.innerHeight
});
}
window.addEventListener('resize', handleResize);
console.log('添加了 resize 监听');
return () => {
window.removeEventListener('resize', handleResize);
console.log('移除了 resize 监听');
};
}, []);
return (
<div style={{
padding: '16px',
backgroundColor: '#f0f0f0',
borderRadius: '8px',
marginTop: '20px'
}}>
<p>窗口尺寸:{size.width} × {size.height}</p>
<small>试试调整浏览器窗口大小</small>
</div>
);
}
function App() {
return (
<div style={{ maxWidth: '600px', margin: '0 auto', padding: '20px' }}>
<h1>useEffect 清理函数</h1>
<PostList />
<WindowSize />
</div>
);
}
ReactDOM.createRoot(document.getElementById('root')).render(<App />);
</script>
</body>
</html>
清理函数执行时机
| 时机 | 说明 |
|---|---|
| 组件卸载时 | 组件从页面消失前执行清理 |
| 依赖变化时 | effect 重新执行前,先清理上一次的 |
需要清理的常见场景
| 场景 | 创建 | 清理 |
|---|---|---|
| 定时器 | setInterval / setTimeout | clearInterval / clearTimeout |
| 事件监听 | addEventListener | removeEventListener |
| 订阅 | subscribe() | unsubscribe() |
⚠️ 新手易错:如果
useEffect 里注册了事件监听或定时器,但忘了返回清理函数,每次重新渲染都会重复注册,导致内存泄漏和性能问题。⚠️ 新手易错:useEffect 的回调不能直接写 async。要写异步请求就在回调内部定义:
JavaScript — async 正确写法
// ❌ 错误
useEffect(async () => { ... }, []);
// ✅ 正确
useEffect(() => {
async function fetchData() { ... }
fetchData();
}, []);
本章复盘——useEffect 核心要点
| 要点 | 说明 |
|---|---|
| 副作用 | 渲染之外的操作:网络请求、DOM 操作、定时器等 |
| useEffect | useEffect(() => {}, [deps]),渲染后执行 |
| 无依赖 | 每次渲染都执行 |
空数组 [] | 仅首次渲染执行一次(初始化数据) |
有依赖 [a, b] | a 或 b 变化时执行 |
| 清理函数 | return () => {},组件卸载或依赖变化时执行 |
| async 用法 | 回调里定义 async 函数再调用,不能直接 async 回调 |
下一章预告(第12章):我们将学习组件通信——父子组件数据传递、回调函数和 Context 上下文。
本文档由 zyyc-0336 编写,最后更新:2026-06-14