第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 / setTimeoutclearInterval / clearTimeout
事件监听addEventListenerremoveEventListener
订阅subscribe()unsubscribe()
⚠️ 新手易错:如果 useEffect 里注册了事件监听或定时器,但忘了返回清理函数,每次重新渲染都会重复注册,导致内存泄漏和性能问题。
⚠️ 新手易错:useEffect 的回调不能直接写 async。要写异步请求就在回调内部定义:
JavaScript — async 正确写法
// ❌ 错误
useEffect(async () => { ... }, []);

// ✅ 正确
useEffect(() => {
    async function fetchData() { ... }
    fetchData();
}, []);

本章复盘——useEffect 核心要点

要点说明
副作用渲染之外的操作:网络请求、DOM 操作、定时器等
useEffectuseEffect(() => {}, [deps]),渲染后执行
无依赖每次渲染都执行
空数组 []仅首次渲染执行一次(初始化数据)
有依赖 [a, b]a 或 b 变化时执行
清理函数return () => {},组件卸载或依赖变化时执行
async 用法回调里定义 async 函数再调用,不能直接 async 回调

下一章预告(第12章):我们将学习组件通信——父子组件数据传递、回调函数和 Context 上下文。

本文档由 zyyc-0336 编写,最后更新:2026-06-14