今天来聊聊 React 19 中的生命周期。如果你之前写过类组件,可能还记得那一堆 componentDidMount、componentWillUnmount 之类的方法。不过现在都 2025 年了,函数组件 + Hooks 早就成为主流,所以今天我们主要聊聊在函数组件中如何优雅地处理生命周期。
先说说什么是生命周期
简单来说,生命周期就是组件从"出生"到"死亡"的整个过程。就像我们人一样,有出生、成长、衰老、死亡,React 组件也是:
创建 → 挂载到页面 → 更新数据 → 再更新 → 最终卸载
在这个过程中的每个阶段,我们可能需要做一些事情,比如:
- 组件刚挂载时去请求数据
- 数据更新后重新计算一些值
- 组件要卸载时清理定时器、取消订阅等
React 19 的生命周期:Hooks 时代
在现代 React 中,我们主要用这几个 Hook 来处理生命周期:
1. useEffect - 最常用的生命周期 Hook
这是你会用得最多的一个。我刚开始学的时候,总把它理解成"副作用",听起来挺高大上,其实说白了就是:在组件渲染后做点事情。
基础用法:挂载时执行
tsx
import { useEffect, useState } from 'react';
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
// 组件挂载后执行
useEffect(() => {
console.log('组件挂载了!');
// 获取用户数据
fetch(`/api/users/${userId}`)
.then(res => res.json())
.then(data => {
setUser(data);
setLoading(false);
});
}, []); // 空数组表示只在挂载时执行一次
if (loading) return <div>加载中...</div>;
return (
<div>
<h1>{user.name}</h1>
<p>{user.email}</p>
</div>
);
}
这里有个坑 :很多新手会忘记写依赖数组 [],结果每次组件重新渲染都会触发 useEffect,然后就陷入无限循环了。我当年也踩过这个坑,debug 了半天才发现 😅
监听变化:依赖数组
tsx
function SearchResults({ keyword }) {
const [results, setResults] = useState([]);
useEffect(() => {
// 只有当 keyword 变化时才执行
console.log(`搜索关键词变了: ${keyword}`);
if (keyword.trim()) {
fetch(`/api/search?q=${keyword}`)
.then(res => res.json())
.then(data => setResults(data));
} else {
setResults([]);
}
}, [keyword]); // 依赖 keyword
return (
<ul>
{results.map(item => (
<li key={item.id}>{item.title}</li>
))}
</ul>
);
}
实战经验:依赖数组里要诚实,用了什么就写什么。别想着偷懒省略,ESLint 会提醒你的(虽然我知道你可能会想关掉提示 😂)。
清理函数:组件卸载时执行
这个特别重要!不做清理会导致内存泄漏,然后你的应用就会越跑越慢。
tsx
function Timer() {
const [seconds, setSeconds] = useState(0);
useEffect(() => {
console.log('定时器开始');
const timer = setInterval(() => {
setSeconds(prev => prev + 1);
}, 1000);
// 返回清理函数
return () => {
console.log('定时器清理');
clearInterval(timer); // 一定要清理!
};
}, []);
return <div>已运行 {seconds} 秒</div>;
}
我踩过的坑:曾经写了个聊天应用,WebSocket 连接没有在组件卸载时关闭,结果用户切换页面后,后台还在疯狂接收消息,最后浏览器直接卡死了。所以清理函数真的很重要!
2. useLayoutEffect - DOM 更新后立即执行
这个跟 useEffect 很像,但执行时机不同:
useEffect:浏览器绘制完成后执行(异步)useLayoutEffect:DOM 更新后、浏览器绘制前执行(同步)
听起来有点绕?我画个图:
状态更新 → DOM 更新 → useLayoutEffect 执行 → 浏览器绘制 → useEffect 执行
什么时候用 useLayoutEffect?
说实话,99% 的情况用 useEffect 就够了。只有在这些场景才需要 useLayoutEffect:
场景 1:需要测量 DOM 尺寸
tsx
import { useLayoutEffect, useRef, useState } from 'react';
function Tooltip({ children }) {
const tooltipRef = useRef(null);
const [position, setPosition] = useState({ top: 0, left: 0 });
useLayoutEffect(() => {
// 立即测量 DOM,避免闪烁
const rect = tooltipRef.current.getBoundingClientRect();
setPosition({
top: rect.bottom + 10,
left: rect.left
});
}, [children]);
return (
<>
<div ref={tooltipRef}>{children}</div>
<div
className="tooltip"
style={{
position: 'fixed',
top: position.top,
left: position.left
}}
>
提示信息
</div>
</>
);
}
如果用 useEffect,用户会看到提示框先出现在错误位置,然后闪一下跳到正确位置,体验很差。用 useLayoutEffect 就能在绘制前计算好位置,避免闪烁。
场景 2:动画前的准备
tsx
function FadeIn({ children }) {
const ref = useRef(null);
useLayoutEffect(() => {
// 设置初始状态(透明)
ref.current.style.opacity = '0';
// 下一帧开始动画
requestAnimationFrame(() => {
ref.current.style.transition = 'opacity 0.3s';
ref.current.style.opacity = '1';
});
}, []);
return <div ref={ref}>{children}</div>;
}
我的建议 :先用 useEffect,如果发现有视觉闪烁或抖动,再改成 useLayoutEffect。不要一上来就用 useLayoutEffect,它是同步的,会阻塞渲染,用多了会让页面卡顿。
3. useInsertionEffect - CSS-in-JS 专用
这个是 React 18 引入的,React 19 继续保留。老实说,除非你在写 CSS-in-JS 库(比如 styled-components、emotion),否则基本用不到。
tsx
import { useInsertionEffect } from 'react';
function useCSS(rule) {
useInsertionEffect(() => {
// 在 DOM 更新前插入样式
const style = document.createElement('style');
style.textContent = rule;
document.head.appendChild(style);
return () => {
document.head.removeChild(style);
};
}, [rule]);
}
执行顺序是:
useInsertionEffect → useLayoutEffect → 浏览器绘制 → useEffect
普通开发者:基本不会用到,知道有这么个东西就行。
4. useEffect 的多种使用模式
模式 1:只在挂载时执行(componentDidMount)
tsx
useEffect(() => {
console.log('我只执行一次!');
// 初始化操作
}, []); // 空依赖数组
模式 2:每次渲染都执行(componentDidUpdate)
tsx
useEffect(() => {
console.log('每次渲染都执行!');
// 通常不推荐这样做
}); // 没有依赖数组
警告:这个要慎用!很容易造成性能问题。
模式 3:监听特定值变化
tsx
useEffect(() => {
console.log('count 变了!');
}, [count]); // 只在 count 变化时执行
模式 4:卸载时清理(componentWillUnmount)
tsx
useEffect(() => {
return () => {
console.log('组件卸载了!');
// 清理操作
};
}, []);
实战场景详解
好了,理论说完了,咱们来看看实际项目中怎么用。
场景 1:数据请求
这是最常见的场景,没有之一。
tsx
function ArticleList() {
const [articles, setArticles] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
// 这个 flag 很重要,防止组件卸载后还在 setState
let isMounted = true;
async function fetchArticles() {
try {
setLoading(true);
const response = await fetch('/api/articles');
const data = await response.json();
// 只有组件还在时才更新状态
if (isMounted) {
setArticles(data);
setError(null);
}
} catch (err) {
if (isMounted) {
setError(err.message);
}
} finally {
if (isMounted) {
setLoading(false);
}
}
}
fetchArticles();
// 清理:标记组件已卸载
return () => {
isMounted = false;
};
}, []);
if (loading) return <div>加载中...</div>;
if (error) return <div>出错了:{error}</div>;
return (
<ul>
{articles.map(article => (
<li key={article.id}>{article.title}</li>
))}
</ul>
);
}
我踩过的坑 :之前没加 isMounted 标志,用户快速切换页面时会报错"Can't perform a React state update on an unmounted component"。加上这个标志后世界清净了。
场景 2:订阅和取消订阅
比如 WebSocket、EventBus、实时数据流等。
tsx
function ChatRoom({ roomId }) {
const [messages, setMessages] = useState([]);
useEffect(() => {
// 连接到聊天室
const socket = new WebSocket(`wss://chat.example.com/${roomId}`);
socket.onopen = () => {
console.log(`已连接到房间 ${roomId}`);
};
socket.onmessage = (event) => {
const message = JSON.parse(event.data);
setMessages(prev => [...prev, message]);
};
socket.onerror = (error) => {
console.error('WebSocket 错误:', error);
};
// 清理:关闭连接
return () => {
console.log(`断开房间 ${roomId} 连接`);
socket.close();
};
}, [roomId]); // roomId 变化时重新连接
return (
<div>
<h2>聊天室 {roomId}</h2>
<div className="messages">
{messages.map((msg, index) => (
<div key={index}>{msg.text}</div>
))}
</div>
</div>
);
}
注意 :依赖数组里写了 roomId,所以当用户切换房间时,会先执行清理函数(关闭旧连接),然后再建立新连接。这个逻辑很优雅!
场景 3:定时器和轮询
tsx
function StockPrice({ symbol }) {
const [price, setPrice] = useState(null);
useEffect(() => {
// 立即获取一次
fetchPrice();
// 每 5 秒轮询一次
const interval = setInterval(() => {
fetchPrice();
}, 5000);
async function fetchPrice() {
try {
const res = await fetch(`/api/stock/${symbol}`);
const data = await res.json();
setPrice(data.price);
} catch (error) {
console.error('获取股价失败:', error);
}
}
// 清理定时器
return () => {
clearInterval(interval);
};
}, [symbol]); // symbol 变化时重新轮询
return (
<div>
{symbol}: ${price ?? '加载中...'}
</div>
);
}
场景 4:监听浏览器事件
tsx
function useWindowSize() {
const [size, setSize] = useState({
width: window.innerWidth,
height: window.innerHeight
});
useEffect(() => {
function handleResize() {
setSize({
width: window.innerWidth,
height: window.innerHeight
});
}
// 添加事件监听
window.addEventListener('resize', handleResize);
// 清理事件监听
return () => {
window.removeEventListener('resize', handleResize);
};
}, []); // 只在挂载时添加,卸载时移除
return size;
}
// 使用
function ResponsiveComponent() {
const { width, height } = useWindowSize();
return (
<div>
窗口大小: {width} x {height}
{width < 768 ? <MobileView /> : <DesktopView />}
</div>
);
}
场景 5:文档标题同步
tsx
function useDocumentTitle(title) {
useEffect(() => {
const prevTitle = document.title;
document.title = title;
// 组件卸载时恢复原标题
return () => {
document.title = prevTitle;
};
}, [title]);
}
// 使用
function ArticlePage({ article }) {
useDocumentTitle(article.title);
return (
<article>
<h1>{article.title}</h1>
<div>{article.content}</div>
</article>
);
}
场景 6:表单自动保存
这个在实际项目中特别有用,用户体验会好很多。
tsx
function AutoSaveForm() {
const [formData, setFormData] = useState({
title: '',
content: ''
});
const [lastSaved, setLastSaved] = useState(null);
// 防抖:内容变化 2 秒后自动保存
useEffect(() => {
const timer = setTimeout(() => {
if (formData.title || formData.content) {
saveToServer(formData);
setLastSaved(new Date());
}
}, 2000);
// 清理上一个定时器
return () => clearTimeout(timer);
}, [formData]); // formData 变化时重新设置定时器
async function saveToServer(data) {
try {
await fetch('/api/drafts', {
method: 'POST',
body: JSON.stringify(data)
});
console.log('自动保存成功');
} catch (error) {
console.error('保存失败:', error);
}
}
return (
<form>
<input
value={formData.title}
onChange={e => setFormData({ ...formData, title: e.target.value })}
placeholder="标题"
/>
<textarea
value={formData.content}
onChange={e => setFormData({ ...formData, content: e.target.value })}
placeholder="内容"
/>
{lastSaved && (
<div className="save-status">
最后保存于 {lastSaved.toLocaleTimeString()}
</div>
)}
</form>
);
}
优化技巧:这里每次 formData 变化都会清除旧定时器、设置新定时器,实现了防抖效果。用户停止输入 2 秒后才会真正发送请求,避免频繁请求服务器。
场景 7:滚动位置恢复
这个在列表页特别有用,用户从详情页返回时能回到之前的滚动位置。
tsx
function ProductList() {
const [products, setProducts] = useState([]);
const scrollPos = useRef(0);
useEffect(() => {
// 恢复滚动位置
window.scrollTo(0, scrollPos.current);
// 监听滚动,保存位置
function handleScroll() {
scrollPos.current = window.scrollY;
}
window.addEventListener('scroll', handleScroll);
return () => {
window.removeEventListener('scroll', handleScroll);
};
}, []);
// 获取数据...
return (
<div className="product-list">
{products.map(product => (
<ProductCard key={product.id} product={product} />
))}
</div>
);
}
场景 8:图片懒加载
tsx
function LazyImage({ src, alt }) {
const imgRef = useRef(null);
const [isVisible, setIsVisible] = useState(false);
useEffect(() => {
// 创建 Intersection Observer
const observer = new IntersectionObserver(
([entry]) => {
if (entry.isIntersecting) {
setIsVisible(true);
observer.disconnect(); // 加载后就不再观察
}
},
{ threshold: 0.1 } // 10% 可见时触发
);
if (imgRef.current) {
observer.observe(imgRef.current);
}
// 清理
return () => {
observer.disconnect();
};
}, []);
return (
<img
ref={imgRef}
src={isVisible ? src : '/placeholder.png'}
alt={alt}
style={{ minHeight: '200px' }}
/>
);
}
常见错误和解决方案
错误 1:无限循环
tsx
// ❌ 错误:会无限循环
function BadComponent() {
const [count, setCount] = useState(0);
useEffect(() => {
setCount(count + 1); // 每次渲染都会触发
}); // 没有依赖数组!
return <div>{count}</div>;
}
// ✅ 正确
function GoodComponent() {
const [count, setCount] = useState(0);
useEffect(() => {
setCount(prev => prev + 1);
}, []); // 只执行一次
}
错误 2:在清理函数中访问过期的状态
tsx
// ❌ 可能有问题
function BadTimer() {
const [count, setCount] = useState(0);
useEffect(() => {
const timer = setInterval(() => {
setCount(count + 1); // count 是闭包中的旧值
}, 1000);
return () => clearInterval(timer);
}, []); // count 不在依赖数组中
}
// ✅ 正确:使用函数式更新
function GoodTimer() {
const [count, setCount] = useState(0);
useEffect(() => {
const timer = setInterval(() => {
setCount(prev => prev + 1); // 使用前一个值
}, 1000);
return () => clearInterval(timer);
}, []);
}
错误 3:忘记清理
tsx
// ❌ 内存泄漏
function BadComponent() {
useEffect(() => {
const subscription = subscribeToData();
// 忘记返回清理函数!
}, []);
}
// ✅ 正确
function GoodComponent() {
useEffect(() => {
const subscription = subscribeToData();
return () => {
subscription.unsubscribe(); // 清理订阅
};
}, []);
}
错误 4:依赖数组不完整
tsx
// ❌ ESLint 会警告
function BadSearch({ userId }) {
const [results, setResults] = useState([]);
useEffect(() => {
fetchUserData(userId); // 使用了 userId
}, []); // 但依赖数组里没有!
}
// ✅ 正确
function GoodSearch({ userId }) {
const [results, setResults] = useState([]);
useEffect(() => {
fetchUserData(userId);
}, [userId]); // 诚实地写上所有依赖
}
进阶技巧
技巧 1:自定义 Hook 封装生命周期逻辑
tsx
// 封装数据请求逻辑
function useFetch(url) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
let isMounted = true;
async function fetchData() {
try {
setLoading(true);
const res = await fetch(url);
const json = await res.json();
if (isMounted) {
setData(json);
setError(null);
}
} catch (err) {
if (isMounted) {
setError(err.message);
}
} finally {
if (isMounted) {
setLoading(false);
}
}
}
fetchData();
return () => {
isMounted = false;
};
}, [url]);
return { data, loading, error };
}
// 使用
function UserProfile({ userId }) {
const { data: user, loading, error } = useFetch(`/api/users/${userId}`);
if (loading) return <div>加载中...</div>;
if (error) return <div>错误: {error}</div>;
return <div>{user.name}</div>;
}
技巧 2:组合多个生命周期
tsx
function ComplexComponent() {
// 挂载时执行
useEffect(() => {
console.log('组件挂载');
return () => console.log('组件卸载');
}, []);
// 监听窗口大小
const windowSize = useWindowSize();
// 监听在线状态
const isOnline = useOnlineStatus();
// 自动保存
useAutoSave(formData);
return <div>...</div>;
}
技巧 3:条件性地执行副作用
tsx
function SearchResults({ query, enabled }) {
const [results, setResults] = useState([]);
useEffect(() => {
// 只在启用且有查询词时执行
if (!enabled || !query) {
return;
}
let isMounted = true;
async function search() {
const data = await fetch(`/api/search?q=${query}`).then(r => r.json());
if (isMounted) {
setResults(data);
}
}
search();
return () => {
isMounted = false;
};
}, [query, enabled]);
return <div>...</div>;
}
性能优化建议
1. 避免在 useEffect 中创建新对象/数组
tsx
// ❌ 每次都创建新对象
useEffect(() => {
const options = { method: 'GET' }; // 新对象
fetch(url, options);
}, [url, options]); // options 每次都不同,会无限循环
// ✅ 使用 useMemo
const options = useMemo(() => ({ method: 'GET' }), []);
useEffect(() => {
fetch(url, options);
}, [url, options]);
2. 使用 AbortController 取消请求
tsx
function SearchBox() {
const [query, setQuery] = useState('');
const [results, setResults] = useState([]);
useEffect(() => {
// 创建 AbortController
const controller = new AbortController();
async function search() {
try {
const res = await fetch(`/api/search?q=${query}`, {
signal: controller.signal // 传入 signal
});
const data = await res.json();
setResults(data);
} catch (err) {
if (err.name === 'AbortError') {
console.log('请求被取消');
} else {
console.error('搜索失败:', err);
}
}
}
if (query) {
search();
}
// 清理:取消请求
return () => {
controller.abort();
};
}, [query]);
return (
<div>
<input
value={query}
onChange={e => setQuery(e.target.value)}
placeholder="搜索..."
/>
<div>{results.map(r => <div key={r.id}>{r.title}</div>)}</div>
</div>
);
}
用户快速输入时,旧的请求会被自动取消,避免资源浪费。
3. 合并多个状态更新
tsx
// ❌ 多次渲染
useEffect(() => {
setLoading(true);
setError(null);
setData(null);
}, []);
// ✅ 使用 useReducer 或对象状态
const [state, setState] = useState({
loading: false,
error: null,
data: null
});
useEffect(() => {
setState({ loading: true, error: null, data: null });
}, []);
调试技巧
1. 使用 console.log 追踪执行
tsx
useEffect(() => {
console.log('📌 useEffect 执行', { userId, timestamp: Date.now() });
return () => {
console.log('🧹 清理函数执行', { userId, timestamp: Date.now() });
};
}, [userId]);
2. 使用 React DevTools Profiler
React DevTools 可以看到组件的渲染次数和原因,帮你找出性能问题。
3. 检查依赖数组
安装 eslint-plugin-react-hooks 插件,它会自动检查你的依赖数组是否完整。
bash
npm install eslint-plugin-react-hooks --save-dev
总结
好了,说了这么多,我们来总结一下:
核心要点
- useEffect 是主力:99% 的生命周期需求都用它
- 依赖数组很重要:用了什么就写什么,别偷懒
- 记得清理:定时器、订阅、事件监听都要清理
- useLayoutEffect 慎用:只在需要同步 DOM 操作时用
- 封装自定义 Hook:复用逻辑,让代码更干净
最佳实践
tsx
function BestPracticeComponent({ id }) {
const [data, setData] = useState(null);
useEffect(() => {
// 1. 使用标志防止卸载后更新状态
let isMounted = true;
// 2. 使用 AbortController 取消请求
const controller = new AbortController();
async function fetchData() {
try {
const res = await fetch(`/api/data/${id}`, {
signal: controller.signal
});
const json = await res.json();
if (isMounted) {
setData(json);
}
} catch (error) {
if (error.name !== 'AbortError') {
console.error(error);
}
}
}
fetchData();
// 3. 返回清理函数
return () => {
isMounted = false;
controller.abort();
};
}, [id]); // 4. 完整的依赖数组
return <div>{data?.title}</div>;
}
我的心得
写了这么多年 React,生命周期这块其实就是要养成好习惯:
- 该清理的一定要清理,不然会埋雷
- 依赖数组要诚实,别跟 ESLint 对着干
- 先想清楚再写,别上来就一顿操作
- 多用自定义 Hook,让逻辑更清晰
希望这篇文章能帮到你!如果有什么问题,欢迎留言讨论。Happy coding! 🚀
参考资料:
写于 : 2025-12-09
最后更新: 2025-12-09
最后,欢迎访问我的个人网站: hixiaohezi.com
