第 8 章:生命周期
学习目标
- 理解函数组件的"生命周期"就是 hooks 调用时机
- 掌握 useEffect 的三种使用模式
- 理解 mount / update / unmount 阶段
- 避开生命周期的 5 个常见坑
一、函数组件的"生命周期"
React 函数组件没有传统 componentDidMount,但每个组件实例经历:
| 阶段 | 含义 | 触发时机 |
|---|---|---|
| Mount | 挂载 | 组件首次渲染到 DOM |
| Update | 更新 | props 或 state 变化导致重渲染 |
| Unmount | 卸载 | 组件从 DOM 移除 |
每个阶段都有对应的 hook:
tsx
function Comp() {
// mount 时执行(每次渲染都跑,默认行为)
useEffect(() => {
console.log('mount 或 update');
return () => {
console.log('清理:update 前/unmount 时');
};
});
// 只在 mount 时执行一次
useEffect(() => {
console.log('只在 mount 时');
}, []);
return <div />;
}二、useEffect 三种模式
模式 1:每次渲染都执行
tsx
useEffect(() => {
console.log('每次渲染');
}); // ⚠️ 不传依赖数组模式 2:依赖变化时执行
tsx
useEffect(() => {
console.log('count 变了:', count);
}, [count]); // ⚠️ 传依赖数组模式 3:只执行一次(mount)
tsx
useEffect(() => {
console.log('只 mount 一次');
}, []); // ⚠️ 空数组三种模式对照
tsx
function App() {
const [count, setCount] = useState(0);
// ① 无依赖 → 每次 render 都跑
useEffect(() => {
console.log('每次渲染');
});
// ② 有依赖 → 依赖变了才跑
useEffect(() => {
console.log('count 变了:', count);
}, [count]);
// ③ 空依赖 → 只 mount 一次,后续 re-render 不再跑
useEffect(() => {
console.log('只跑一次');
}, []);
return <button onClick={() => setCount(c => c + 1)}>{count}</button>;
}| 操作 | ① 无依赖 | ② [count] | ③ [] |
|---|---|---|---|
| 首次渲染 | 输出 | 输出 | 输出 |
| 点 1 次(count 变 1) | 输出 | 输出 | — |
| 点 2 次(count 变 2) | 输出 | 输出 | — |
| 点 100 次 | 输出 100 次 | 输出 100 次 | 永远只输出 1 次 |
[] 的语义:只在 mount 时跑一次,后续 re-render 不再触发——不依赖任何 state/props,所以 React 没理由再执行它。
[] 的典型用法
tsx
useEffect(() => {
// ① 初始化(只搞一次)
initChart();
loadConfig();
// ② 订阅(WebSocket / EventBus / 全局事件)
const ws = new WebSocket('wss://...');
ws.onmessage = handleMsg;
// ③ 清理(组件卸载时跑)
return () => ws.close(); // 防内存泄漏
}, []);⚠️ 坑 4:
[]里别读 state——会拿到 mount 时的旧值,后续变化它看不见:tsxconst [count, setCount] = useState(0); useEffect(() => { console.log(count); // ❌ 永远是 0(mount 时的快照) }, []);解法(二选一):
tsx// ① 加依赖(count 变了再跑) useEffect(() => { console.log(count); }, [count]); // ② 用 ref 同步最新值 const countRef = useRef(count); countRef.current = count; // 每次 render 同步 useEffect(() => { console.log(countRef.current); // ✅ 总是最新 }, []);💡
[]= 只跑一次 + 卸载清理——挂载初始化、订阅事件、定时器。别在里面读 state(闭包陷阱)。
三、清理函数(Cleanup)
useEffect 可以返回一个函数,React 在重新执行 effect 前或组件卸载时调用它:
tsx
useEffect(() => {
const id = setInterval(() => console.log('tick'), 1000);
return () => {
clearInterval(id); // ⚠️ 必须清理,否则内存泄漏
};
}, []);常见需要清理的资源:
setInterval/setTimeout- 事件监听器(
addEventListener) - WebSocket 连接
- 订阅(Redux/Zustand)
⚠️ 坑 1:用了
setInterval不 cleanup,组件卸载后定时器仍在跑,内存泄漏 + 报错"Can't perform state update on unmounted"。
四、生命周期对应关系
| 类组件 | 函数组件 |
|---|---|
componentDidMount | useEffect(fn, []) |
componentDidUpdate | useEffect(fn, [deps]) |
componentWillUnmount | useEffect(() => () => cleanup, []) |
shouldComponentUpdate | React.memo / useMemo |
getDerivedStateFromProps | useState + useEffect |
五、useLayoutEffect(同步版)
与 useEffect 类似,但在 DOM 更新后、浏览器绘制前同步执行:
tsx
useLayoutEffect(() => {
// 同步读 DOM 布局(如读元素宽度然后设置)
const w = ref.current?.offsetWidth;
setWidth(w);
}, []);99% 场景用
useEffect,只有需要同步读 DOM 布局时才用useLayoutEffect。
六、Strict Mode 双调用
开发模式下 <StrictMode> 会故意mount → unmount → mount 跑一遍,验证 cleanup 是否正确:
tsx
useEffect(() => {
console.log('A:订阅');
return () => console.log('A:取消订阅');
}, []);输出:A:订阅 → A:取消订阅 → A:订阅(故意触发,验证 cleanup 工作正常)
⚠️ 坑 2:不要在 useEffect 里调只执行一次的初始化(比如埋点上报),否则 Strict Mode 会双触发。生产无影响。
七、常见场景代码
监听窗口尺寸
tsx
function useWindowSize() {
const [size, setSize] = useState({ w: window.innerWidth, h: window.innerHeight });
useEffect(() => {
function update() {
setSize({ w: window.innerWidth, h: window.innerHeight });
}
window.addEventListener('resize', update);
return () => window.removeEventListener('resize', update);
}, []);
return size;
}拉数据
tsx
function UserList() {
const [users, setUsers] = useState<User[]>([]);
useEffect(() => {
fetch('/api/users')
.then(r => r.json())
.then(setUsers);
}, []); // ⚠️ 空依赖,只拉一次
}⚠️ 坑 3:依赖数组漏写依赖会导致 effect 用到旧值。用 ESLint 插件
exhaustive-deps帮你检查。
八、本章小结
| 要点 | 关键 |
|---|---|
| 三种模式 | 不传 / [dep] / [] |
| Cleanup | 返回函数,清理定时器/订阅 |
| mount | useEffect(fn, []) |
| unmount | cleanup 在 unmount 时触发 |
| Strict Mode | 开发模式故意双 mount 验证 cleanup |
| useLayoutEffect | 极少数需要同步 DOM 的场景 |
| 数据拉取 | mount 时一次,加 cleanup 防泄漏 |
动手练习
- 计时器:做一个秒表组件,开始/停止按钮,停止时清理定时器
- 窗口尺寸:写一个
useWindowSizehook(见上),在组件里展示宽高 - 拉数据:写一个组件,挂载时 fetch 一次用户列表,展示 loading/数据/错误三态
- 找坑:以下代码有什么问题?tsx
useEffect(() => { const id = setInterval(() => console.log(count), 1000); }, []);
下一章:第 9 章:Hooks 基础 →