生命周期是指组件从创建到销毁的完整过程。React组件在不同阶段会调用特定的方法,这些方法被称为生命周期方法。
组件被创建并插入到DOM中的过程
组件重新渲染的过程,通常由props或state的变化引起
组件从DOM中移除的过程
在Hooks出现之前,生命周期方法只能在类组件中使用。了解这些方法有助于理解useEffect的工作方式。
import React from 'react';
class LifecycleDemo extends React.Component {
constructor(props) {
super(props);
this.state = {
count: 0,
data: null
};
console.log('constructor: 组件初始化');
}
componentDidMount() {
console.log('componentDidMount: 组件已挂载');
// 通常在这里进行数据获取
this.fetchData();
}
componentDidUpdate(prevProps, prevState) {
console.log('componentDidUpdate: 组件已更新');
// 检查count是否变化
if (prevState.count !== this.state.count) {
console.log(`count从 ${prevState.count} 变为 ${this.state.count}`);
}
}
componentWillUnmount() {
console.log('componentWillUnmount: 组件即将卸载');
// 清理工作,如清除定时器、取消订阅等
clearInterval(this.timerId);
}
fetchData = async () => {
// 模拟API调用
const response = await fetch('https://api.example.com/data');
const data = await response.json();
this.setState({ data });
};
increment = () => {
this.setState(prevState => ({
count: prevState.count + 1
}));
};
render() {
console.log('render: 渲染组件');
return (
<div>
<h3>计数器: {this.state.count}</h3>
<button onClick={this.increment}>增加</button>
{this.state.data && (
<p>数据: {JSON.stringify(this.state.data)}</p>
)}
</div>
);
}
}
useEffect是React提供的Hook,用于在函数组件中执行副作用操作。它可以看作是componentDidMount、componentDidUpdate和componentWillUnmount的组合。
import React, { useState, useEffect } from 'react';
function Example() {
const [count, setCount] = useState(0);
// useEffect的基本用法
useEffect(() => {
// 这里的代码在每次渲染后执行
console.log(`计数已更新: ${count}`);
// 清理函数(可选)
return () => {
console.log('清理上一次的effect');
};
});
return (
<div>
<p>当前计数: {count}</p>
<button onClick={() => setCount(count + 1)}>
点击增加
</button>
</div>
);
}
useEffect的第二个参数是一个依赖数组,用于控制effect的执行时机:
effect只在组件挂载和卸载时执行
useEffect(() => {
console.log('只执行一次');
}, []);
当依赖项变化时执行
useEffect(() => {
console.log('count变化时执行');
}, [count]);
每次渲染后都执行
useEffect(() => {
console.log('每次渲染都执行');
});
| 类组件生命周期方法 | useEffect等效写法 | 说明 |
|---|---|---|
componentDidMount() |
|
空依赖数组确保effect只在挂载时运行一次 |
componentDidUpdate(prevProps, prevState) |
|
无依赖数组每次渲染都执行,有依赖数组在依赖变化时执行 |
componentWillUnmount() |
|
返回一个清理函数,在组件卸载时执行 |
shouldComponentUpdate() |
|
使用React.memo进行性能优化 |
import React, { useState, useEffect } from 'react';
function DataFetching() {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
// 定义异步函数
const fetchData = async () => {
try {
setLoading(true);
const response = await fetch('https://api.example.com/posts');
if (!response.ok) {
throw new Error('网络响应异常');
}
const result = await response.json();
setData(result);
setError(null);
} catch (err) {
setError(err.message);
} finally {
setLoading(false);
}
};
fetchData();
// 清理函数:取消请求(如果支持AbortController)
return () => {
// 这里可以添加取消请求的逻辑
};
}, []); // 空数组:只执行一次
if (loading) return <div>加载中...</div>;
if (error) return <div>错误: {error}</div>;
return (
<div>
<h3>数据列表</h3>
<ul>
{data && data.map(item => (
<li key={item.id}>{item.title}</li>
))}
</ul>
</div>
);
}
在useEffect中直接使用async函数会导致问题,因为useEffect的清理函数需要同步执行。应该将async函数定义在effect内部或外部,然后在effect中调用它。
import React, { useState, useEffect } from 'react';
function WindowResizeTracker() {
const [windowSize, setWindowSize] = useState({
width: window.innerWidth,
height: window.innerHeight
});
useEffect(() => {
// 处理窗口大小变化的函数
const handleResize = () => {
setWindowSize({
width: window.innerWidth,
height: window.innerHeight
});
};
// 添加事件监听
window.addEventListener('resize', handleResize);
// 立即调用一次以获取初始尺寸
handleResize();
// 清理函数:移除事件监听
return () => {
window.removeEventListener('resize', handleResize);
};
}, []); // 空数组:只在挂载和卸载时执行
return (
<div>
<h3>窗口尺寸监控</h3>
<p>宽度: {windowSize.width}px</p>
<p>高度: {windowSize.height}px</p>
<p>设备类型: {windowSize.width < 768 ? '手机' : windowSize.width < 1024 ? '平板' : '桌面'}</p>
</div>
);
}
import React, { useState, useEffect } from 'react';
function Timer() {
const [time, setTime] = useState(0);
const [isRunning, setIsRunning] = useState(false);
useEffect(() => {
let timerId = null;
if (isRunning) {
timerId = setInterval(() => {
setTime(prevTime => prevTime + 1);
}, 1000);
}
// 清理函数:清除定时器
return () => {
if (timerId) {
clearInterval(timerId);
}
};
}, [isRunning]); // 依赖isRunning:当isRunning变化时重新执行
const formatTime = (seconds) => {
const mins = Math.floor(seconds / 60);
const secs = seconds % 60;
return `${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`;
};
return (
<div className="timer">
<h3>定时器: {formatTime(time)}</h3>
<div className="timer-controls">
<button onClick={() => setIsRunning(!isRunning)}>
{isRunning ? '暂停' : '开始'}
</button>
<button onClick={() => setTime(0)} disabled={isRunning}>
重置
</button>
</div>
</div>
);
}
import React, { useState, useEffect } from 'react';
// 模拟外部数据源
class MockWebSocket {
constructor(url) {
this.url = url;
this.listeners = [];
}
connect() {
console.log(`连接到 ${this.url}`);
this.connected = true;
}
disconnect() {
console.log(`断开连接 ${this.url}`);
this.connected = false;
}
subscribe(callback) {
this.listeners.push(callback);
return () => {
this.listeners = this.listeners.filter(listener => listener !== callback);
};
}
// 模拟发送消息
simulateMessage(message) {
this.listeners.forEach(listener => listener(message));
}
}
function WebSocketComponent() {
const [messages, setMessages] = useState([]);
const [connectionStatus, setConnectionStatus] = useState('未连接');
useEffect(() => {
// 创建WebSocket连接
const ws = new MockWebSocket('ws://api.example.com/ws');
ws.connect();
setConnectionStatus('已连接');
// 订阅消息
const unsubscribe = ws.subscribe((message) => {
setMessages(prev => [...prev, message]);
});
// 模拟收到消息
const intervalId = setInterval(() => {
if (ws.connected) {
ws.simulateMessage({
id: Date.now(),
content: `消息 ${messages.length + 1}`,
timestamp: new Date().toISOString()
});
}
}, 3000);
// 清理函数:断开连接和取消订阅
return () => {
console.log('清理WebSocket连接');
clearInterval(intervalId);
ws.disconnect();
setConnectionStatus('已断开');
unsubscribe();
};
}, []); // 空数组:只执行一次
return (
<div>
<h3>WebSocket消息</h3>
<p>连接状态: {connectionStatus}</p>
<ul>
{messages.map(msg => (
<li key={msg.id}>
{msg.content} ({new Date(msg.timestamp).toLocaleTimeString()})
</li>
))}
</ul>
</div>
);
}
可以将不同的副作用逻辑分离到多个useEffect中,使代码更清晰:
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
const [posts, setPosts] = useState([]);
const [onlineStatus, setOnlineStatus] = useState('离线');
// 获取用户信息
useEffect(() => {
fetch(`/api/users/${userId}`)
.then(res => res.json())
.then(data => setUser(data));
}, [userId]); // 当userId变化时重新获取
// 获取用户帖子
useEffect(() => {
fetch(`/api/users/${userId}/posts`)
.then(res => res.json())
.then(data => setPosts(data));
}, [userId]);
// 监听在线状态
useEffect(() => {
const handleOnline = () => setOnlineStatus('在线');
const handleOffline = () => setOnlineStatus('离线');
window.addEventListener('online', handleOnline);
window.addEventListener('offline', handleOffline);
return () => {
window.removeEventListener('online', handleOnline);
window.removeEventListener('offline', handleOffline);
};
}, []); // 空数组:只执行一次
// 更多useEffect...
}
function ExpensiveComponent({ data, filter }) {
const [filteredData, setFilteredData] = useState([]);
useEffect(() => {
// 只有当data或filter变化时才执行昂贵的过滤操作
console.log('执行昂贵的过滤操作...');
const result = data.filter(item => {
// 模拟复杂过滤逻辑
return item.name.includes(filter);
});
setFilteredData(result);
}, [data, filter]); // 依赖数组:只在data或filter变化时执行
return (
<div>
<p>过滤条件: {filter}</p>
<p>结果数量: {filteredData.length}</p>
</div>
);
}
function CounterWithLog() {
const [count, setCount] = useState(0);
const [log, setLog] = useState([]);
useEffect(() => {
// 使用函数式更新确保获取最新的state
setLog(prevLog => [
...prevLog,
`计数更新为: ${count} (时间: ${new Date().toLocaleTimeString()})`
]);
}, [count]); // 依赖count:当count变化时执行
return (
<div>
<h3>计数器: {count}</h3>
<button onClick={() => setCount(count + 1)}>增加</button>
<div>
<h4>更新日志:</h4>
<ul>
{log.map((entry, index) => (
<li key={index}>{entry}</li>
))}
</ul>
</div>
</div>
);
}
当effect中更新了依赖的状态,但没有正确设置依赖数组时,会导致无限循环:
// ❌ 错误:导致无限循环
const [count, setCount] = useState(0);
useEffect(() => {
setCount(count + 1); // 更新count,触发重新渲染
}); // 没有依赖数组,每次渲染后都执行
// ✅ 正确:使用空数组或确保逻辑正确
useEffect(() => {
// 只执行一次的初始化逻辑
}, []); // 空数组:只执行一次
如果effect中使用了某个状态或prop,但依赖数组中遗漏了它,可能导致bug:
const [count, setCount] = useState(0);
const [multiplier, setMultiplier] = useState(2);
// ❌ 错误:遗漏了multiplier依赖
useEffect(() => {
console.log(`结果: ${count * multiplier}`);
}, [count]); // 遗漏了multiplier
// ✅ 正确:包含所有依赖
useEffect(() => {
console.log(`结果: ${count * multiplier}`);
}, [count, multiplier]); // 包含所有依赖
对于需要清理的副作用(如事件监听、定时器、订阅),必须提供清理函数:
// ❌ 错误:可能导致内存泄漏
useEffect(() => {
window.addEventListener('resize', handleResize);
}, []);
// ✅ 正确:提供清理函数
useEffect(() => {
window.addEventListener('resize', handleResize);
return () => {
window.removeEventListener('resize', handleResize);
};
}, []);
import React, { useState, useEffect, useCallback } from 'react';
function DataDashboard() {
const [dashboardData, setDashboardData] = useState({
users: 0,
posts: 0,
comments: 0,
lastUpdated: null
});
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
const [autoRefresh, setAutoRefresh] = useState(false);
// 获取数据函数
const fetchDashboardData = useCallback(async () => {
setLoading(true);
setError(null);
try {
// 模拟并行获取多个数据源
const [usersRes, postsRes, commentsRes] = await Promise.all([
fetch('/api/users/count'),
fetch('/api/posts/count'),
fetch('/api/comments/count')
]);
if (!usersRes.ok || !postsRes.ok || !commentsRes.ok) {
throw new Error('获取数据失败');
}
const usersCount = await usersRes.json();
const postsCount = await postsRes.json();
const commentsCount = await commentsRes.json();
setDashboardData({
users: usersCount,
posts: postsCount,
comments: commentsCount,
lastUpdated: new Date().toISOString()
});
} catch (err) {
setError(err.message);
console.error('获取数据失败:', err);
} finally {
setLoading(false);
}
}, []);
// 初始加载和手动刷新
useEffect(() => {
fetchDashboardData();
}, [fetchDashboardData]);
// 自动刷新定时器
useEffect(() => {
let intervalId = null;
if (autoRefresh) {
intervalId = setInterval(() => {
console.log('自动刷新数据...');
fetchDashboardData();
}, 30000); // 每30秒刷新一次
}
return () => {
if (intervalId) {
clearInterval(intervalId);
}
};
}, [autoRefresh, fetchDashboardData]);
// 窗口焦点时刷新
useEffect(() => {
const handleFocus = () => {
if (document.visibilityState === 'visible') {
console.log('窗口重新获得焦点,刷新数据');
fetchDashboardData();
}
};
document.addEventListener('visibilitychange', handleFocus);
return () => {
document.removeEventListener('visibilitychange', handleFocus);
};
}, [fetchDashboardData]);
// 格式化时间
const formatTime = (isoString) => {
if (!isoString) return '未更新';
return new Date(isoString).toLocaleString();
};
return (
<div className="data-dashboard">
<h2>数据仪表盘</h2>
<div className="dashboard-controls">
<button
onClick={fetchDashboardData}
disabled={loading}
className="btn btn-primary"
>
{loading ? '加载中...' : '刷新数据'}
</button>
<label>
<input
type="checkbox"
checked={autoRefresh}
onChange={(e) => setAutoRefresh(e.target.checked)}
/>
自动刷新 (30秒)
</label>
</div>
{error && (
<div className="alert alert-danger">
错误: {error}
</div>
)}
<div className="dashboard-stats">
<div className="stat-card">
<h3>用户数</h3>
<p className="stat-value">{dashboardData.users.toLocaleString()}</p>
</div>
<div className="stat-card">
<h3>帖子数</h3>
<p className="stat-value">{dashboardData.posts.toLocaleString()}</p>
</div>
<div className="stat-card">
<h3>评论数</h3>
<p className="stat-value">{dashboardData.comments.toLocaleString()}</p>
</div>
</div>
<div className="dashboard-footer">
<p>最后更新: {formatTime(dashboardData.lastUpdated)}</p>
{loading && <p className="text-muted">数据加载中...</p>}
</div>
</div>
);
}