生命周期与 useEffect
组件从"出生"(挂载)、"长大"(更新)到"死亡"(卸载),这个过程叫生命周期。在这些时刻,我们常常需要做点"额外的事":挂载后发请求、更新后同步标题、卸载前清理定时器。React 用 useEffect 这个 Hook 统一管理这些"副作用"——这是 React 第二重要的 Hook(仅次于 useState)。
1. 什么是副作用
React 组件的本职是"根据 props/state 渲染 UI",纯函数,不该有外部影响。但真实应用里,组件不可避免要"和外部世界打交道":
// useEffect:在"渲染完成后"执行副作用
import { useState, useEffect } from "react";
function Example() {
const [count, setCount] = useState(0);
// 组件每次渲染完,这个函数都会跑一遍(默认行为)
useEffect(() => {
document.title = `点击了 ${count} 次`;
});
return <button onClick={() => setCount(count + 1)}>+1</button>;
}
// 副作用 = 和"外部世界"打交道的事:
// 改 document.title、发请求、开定时器、订阅事件、操作 DOM 等这些操作(发请求、改 DOM、开定时器、订阅事件)统称副作用(side effect)。React 不允许你把它们写在组件主体里(渲染时不该有副作用),而是要求用 useEffect 包起来——渲染完成后才执行。
2. 三种执行时机
useEffect 的第二个参数(依赖数组)决定它什么时候执行。这是这个 Hook 的核心难点:
import { useState, useEffect } from "react";
function Demo({ userId }) {
const [data, setData] = useState(null);
// 情况 1:不传第二个参数 → 每次渲染后都跑(几乎不用,性能差)
useEffect(() => {
console.log("每次渲染都跑");
});
// 情况 2:传空数组 [] → 只在"挂载"时跑一次(常用于初始请求)
useEffect(() => {
console.log("组件挂载了,只跑一次");
fetchData();
}, []);
// 情况 3:传依赖数组 [deps] → 挂载时 + 依赖变化时跑
useEffect(() => {
console.log("userId 变了,重新请求");
fetch(`/api/users/${userId}`).then(setData);
}, [userId]);
// 第一次挂载会跑,之后只有 userId 变化才重跑
return <div>{data ? data.name : "加载中"}</div>;
}记忆口诀:不传 → 每次渲染都跑(几乎不用);空数组 → 只挂载时跑一次;有依赖 → 依赖变才重跑。日常 90% 的场景,你要么用空数组(初始请求),要么用依赖数组(数据驱动副作用)。
3. 清理函数:卸载时的扫尾
副作用经常有"扫尾"需求:开的定时器要关、订阅的事件要退、监听的 WebSocket 要断。如果不清理,组件卸载后这些资源还在跑,导致内存泄漏和诡异 bug。useEffect 用返回一个清理函数来处理:
import { useState, useEffect } from "react";
function Timer() {
const [seconds, setSeconds] = useState(0);
useEffect(() => {
// 开启定时器(副作用)
const id = setInterval(() => {
setSeconds(s => s + 1);
}, 1000);
// 返回一个"清理函数":组件卸载或重新跑 effect 之前执行
return () => {
clearInterval(id); // 关掉定时器,避免内存泄漏!
};
}, []); // 空数组:只挂载时开定时器,卸载时清理
return <p>已运行 {seconds} 秒</p>;
}
// 不清理的后果:
// 组件卸载后定时器还在跑 → 不停改 state → 报错 "Can't update unmounted component"
// 内存泄漏 + 性能问题清理函数的执行时机:(1) 组件卸载时;(2) effect 下次重新执行前(清理上一次的副作用)。所以无论依赖怎么变,副作用都不会"堆积",每次都是干净的。
4. 经典副作用模式
记住这三种最常见的模式,日常开发覆盖 90%:
// 模式 1:数据请求(挂载时 + 依赖变化时)
useEffect(() => {
let cancelled = false;
fetch(`/api/users/${userId}`)
.then(res => res.json())
.then(data => {
if (!cancelled) setUser(data); // 防止卸载后还 setState
});
return () => { cancelled = true; }; // 卸载时标记取消
}, [userId]);
// 模式 2:订阅事件(挂载时订阅,卸载时退订)
useEffect(() => {
const handleResize = () => setWidth(window.innerWidth);
window.addEventListener("resize", handleResize);
return () => window.removeEventListener("resize", handleResize);
}, []);
// 模式 3:操作 DOM(挂载后操作,因为这时 DOM 已经渲染好)
useEffect(() => {
const canvas = canvasRef.current;
const ctx = canvas.getContext("2d");
drawChart(ctx, data);
}, [data]);模式 1(数据请求)里那个 cancelled 标志很重要:组件卸载后请求才返回,这时再 setState 会报错。用 cancelled 标志跳过,是最简单的解法。更现代的做法是用 AbortController 直接取消请求。
5. 三个经典陷阱
// 陷阱 1:无限循环
const [count, setCount] = useState(0);
useEffect(() => {
setCount(count + 1); // 改 state → 触发重渲染 → effect 再跑 → 再改 → 死循环!
}); // 没有依赖数组 = 每次渲染都跑
// 陷阱 2:漏写依赖 → 用到旧值(闭包陷阱)
useEffect(() => {
const id = setInterval(() => {
console.log(count); // count 永远是 0!effect 只跑一次,捕获了旧的 count
}, 1000);
return () => clearInterval(id);
}, []); // 漏了 count
// 陷阱 3:把函数/对象当依赖
useEffect(() => {
doSomething(handleClick); // handleClick 每次渲染都是新函数
}, [handleClick]); // → effect 每次都跑,等于没优化- 无限循环:在 effect 里 setState 又不传依赖数组 → state 变 → 重渲染 → effect 再跑 → 再 setState → 死循环,页面卡死。解决:加依赖数组,或检查 setState 是否必要。
- 闭包陷阱:effect 只在挂载时跑一次,内部用到的变量永远是"挂载时的旧值"。解决:把用到的变量写进依赖数组。
- 对象/函数当依赖:每次渲染都是新引用,effect 每次都跑。解决:用
useCallback/useMemo稳定引用,或把依赖挪走。
6. 装个 ESLint 插件自动检查依赖
新手最容易漏写依赖。React 官方提供了 eslint-plugin-react-hooks,它会自动检测你 effect 里用了哪些变量、有没有漏写进依赖数组——漏了就报黄字警告。配 VS Code 后,写错当场红字提示。这个插件是 React 项目的标配,装好它,你能少踩 90% 的 effect 坑。
7. 对比 class 生命周期(了解)
老教程里你会看到 class 组件的三个生命周期方法。函数组件的 useEffect 把它们统一了:
// React class 组件的生命周期(对比了解,新代码用函数组件)
class Comp extends React.Component {
componentDidMount() { /* 挂载后,等价于 useEffect(..., []) */ }
componentDidUpdate() { /* 更新后,等价于 useEffect(..., [deps]) */ }
componentWillUnmount() { /* 卸载前,等价于 useEffect 的 return 清理 */ }
}
// 函数组件用 useEffect 统一了这三个:
useEffect(() => {
// 挂载 + 更新 时干的事
return () => {
// 卸载 + 下次更新前 清理的事
};
}, [deps]);新代码一律用函数组件 + Hooks,不用学 class。但看老项目源码时要能对应得上。componentDidMount = useEffect(fn, []),componentWillUnmount = effect 的 return 清理函数——记住这两个对应关系就够。
8. 一个常见误区:effect 不是事件
新手常把 effect 当"事件监听器"用,什么逻辑都往里塞。记住:effect 是"状态变化后要同步做的事",不是"用户操作后要做的事"。用户点击、提交这种交互,直接在 onClick 里处理,不要绕到 effect。只有"props/state 变化 → 需要同步到外部"(如发请求、改标题)才用 effect。这个心智模型对了,你的 React 代码就会清爽很多。
小结
useEffect 管理副作用,三种时机:不传每次跑、空数组挂载时跑、依赖数组依赖变时跑。副作用有清理需求时返回一个函数。装 eslint-plugin-react-hooks 自动查依赖。掌握 useState + useEffect,你就掌握了现代 React 90% 的日常开发。
恭喜,React 系列到这里就结束了!回到 React 教程目录复习,或继续看我们的其他教程。