在React的世界里,组件主要职责是渲染UI,就像演员在舞台上表演。但有些工作需要在"幕后"完成,比如联系后台取道具、调整舞台灯光、安排观众入场,这些就是所谓的"副作用"。
useEffect就是React雇佣的专业"幕后工作者",负责处理与渲染无关的"副作用"任务。
想象一下React组件是一个繁忙的办公室:
[]
空依赖数组[dep1, dep2]
有依赖项类似于componentDidMount
// 类组件写法
class WelcomeSign extends React.Component {
componentDidMount() {
document.title = 'Welcome to our store!';
this.timer = setInterval(() => {
console.log('Store is open');
}, 1000);
}
componentWillUnmount() {
clearInterval(this.timer);
}
render() {
return Our Store
;
}
}
// useEffect写法
function WelcomeSign() {
useEffect(() => {
// 组件挂载后执行,类似componentDidMount
document.title = 'Welcome to our store!';
const timer = setInterval(() => {
console.log('Store is open');
}, 1000);
// 返回清理函数,类似componentWillUnmount
return () => {
clearInterval(timer);
};
}, []); // 空依赖数组表示只执行一次
return Our Store
;
}
生活类比:这就像清洁工第一天入职时,会进行一次全面清洁,并设置好每天的自动喷香装置。当清洁工离职时,会关闭这些装置并进行最后的整理。
类似于componentDidUpdate(带条件检查)
// 类组件写法
class UserProfile extends React.Component {
componentDidMount() {
this.fetchUserData(this.props.userId);
}
componentDidUpdate(prevProps) {
// 条件判断避免无限循环
if (prevProps.userId !== this.props.userId) {
this.fetchUserData(this.props.userId);
}
}
fetchUserData(userId) {
// 获取用户数据
}
render() {
// 渲染用户资料
}
}
// useEffect写法
function UserProfile({ userId }) {
useEffect(() => {
// 当userId变化时执行,结合了componentDidMount和componentDidUpdate
console.log(`Fetching data for user ${userId}`);
fetchUserData(userId);
// 可选的清理函数
return () => {
console.log(`Cancelling request for user ${userId}`);
// 取消之前的请求
};
}, [userId]); // 依赖数组指定了触发条件
// 渲染用户资料
}
生活类比:这就像清洁工看到"清洁条件检查表",当表上的特定项目(比如地板脏了、垃圾桶满了)有变化时,才会执行相应的清洁工作。
类似于componentWillUnmount
// 类组件写法
class ChatRoom extends React.Component {
componentDidMount() {
ChatAPI.subscribe(this.props.roomId);
}
componentWillUnmount() {
ChatAPI.unsubscribe(this.props.roomId);
}
render() {
return Chat Room: {this.props.roomId};
}
}
// useEffect写法
function ChatRoom({ roomId }) {
useEffect(() => {
// 订阅
ChatAPI.subscribe(roomId);
// 返回清理函数,组件卸载时执行
return () => {
ChatAPI.unsubscribe(roomId);
};
}, [roomId]); // 依赖roomId
return Chat Room: {roomId};
}
生活类比:这就像当公司决定搬迁办公室时,清洁工会进行最后的清扫和设备拆除,确保不留下任何垃圾或未关闭的设备。
想象当roomId
从"general"变为"help"时会发生什么:
ChatAPI.unsubscribe("general")
ChatAPI.subscribe("help")
function ProductPage({ productId }) {
const [product, setProduct] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
// 重置状态
setLoading(true);
setError(null);
// 声明异步函数
async function fetchProduct() {
try {
const response = await fetch(`/api/products/${productId}`);
if (!response.ok) throw new Error('Failed to fetch');
const data = await response.json();
setProduct(data);
} catch (err) {
setError(err.message);
} finally {
setLoading(false);
}
}
// 执行异步函数
fetchProduct();
// 可选的清理函数(取消请求)
return () => {
// 如果使用了可取消的请求,这里可以取消
console.log('Cleanup: cancelling product fetch');
};
}, [productId]); // 依赖productId
if (loading) return Loading...;
if (error) return Error: {error};
if (!product) return null;
return (
{product.name}
{product.description}
${product.price}
);
}
function KeyPressListener() {
const [pressedKeys, setPressedKeys] = useState([]);
useEffect(() => {
// 监听函数
const handleKeyDown = (event) => {
setPressedKeys(prev =>
prev.includes(event.key) ? prev : [...prev, event.key]
);
};
const handleKeyUp = (event) => {
setPressedKeys(prev => prev.filter(key => key !== event.key));
};
// 添加事件监听
window.addEventListener('keydown', handleKeyDown);
window.addEventListener('keyup', handleKeyUp);
// 清理函数 - 移除事件监听
return () => {
window.removeEventListener('keydown', handleKeyDown);
window.removeEventListener('keyup', handleKeyUp);
};
}, []); // 空依赖数组,只设置一次监听
return (
Pressed Keys:
{pressedKeys.length ? (
{pressedKeys.map(key => - {key}
)}
) : (
Press any key...
)}
);
}
function UserDashboard({ userId }) {
// 每个Effect处理一个独立的关注点
// Effect 1: 修改页面标题
useEffect(() => {
document.title = `Dashboard for User ${userId}`;
return () => {
document.title = 'User Dashboard';
};
}, [userId]);
// Effect 2: 加载用户数据
useEffect(() => {
// 加载用户数据...
}, [userId]);
// Effect 3: 活动状态追踪
useEffect(() => {
const lastActive = Date.now();
const updateActivity = () => {
console.log('User active');
// 更新用户活动状态...
};
window.addEventListener('click', updateActivity);
window.addEventListener('keypress', updateActivity);
// 定期发送活动状态
const intervalId = setInterval(() => {
sendActivityStatus(userId, lastActive);
}, 60000);
return () => {
window.removeEventListener('click', updateActivity);
window.removeEventListener('keypress', updateActivity);
clearInterval(intervalId);
};
}, [userId]); // 当用户ID变化时重新设置
return Dashboard content...;
}
在现代React中,更好的理解useEffect的方式是将其视为"同步"而非生命周期事件。
生活类比:不要把useEffect看作是特定时间点的触发器,而要看作是保持两个世界同步的机制 - 就像保持实体书店库存与在线系统同步。
// 同步思维模型示例
function ProfileWithAvatar({ userId }) {
const [user, setUser] = useState(null);
// 这个Effect保持"组件状态"与"服务器数据"同步
useEffect(() => {
// 当userId变化时,同步用户数据
if (userId) {
fetchUser(userId).then(data => setUser(data));
}
}, [userId]);
// 这个Effect保持"文档标题"与"用户状态"同步
useEffect(() => {
// 当user变化时,同步文档标题
if (user) {
document.title = `${user.name}'s Profile`;
return () => {
document.title = 'User Profile';
};
}
}, [user]);
if (!user) return ;
return (
{user.name}
);
}
function ConditionalEffect({ shouldRun, data }) {
useEffect(() => {
// 只有当shouldRun为true时才执行
if (!shouldRun) return;
console.log('Running effect with:', data);
// 执行副作用...
return () => {
console.log('Cleaning up effect');
// 清理副作用...
};
}, [shouldRun, data]); // 依赖项包含shouldRun
return Component content;
}
function SearchResults({ query }) {
const [results, setResults] = useState([]);
const [loading, setLoading] = useState(false);
useEffect(() => {
// 跳过空查询
if (!query.trim()) return;
let isMounted = true; // 追踪组件是否仍然挂载
setLoading(true);
fetchSearchResults(query)
.then(data => {
// 只有当前Effect仍然"活跃"时才更新状态
if (isMounted) {
setResults(data);
setLoading(false);
}
})
.catch(error => {
if (isMounted) {
console.error('Search error:', error);
setLoading(false);
}
});
// 清理函数
return () => {
isMounted = false; // 标记组件已卸载或依赖已变化
};
}, [query]);
return (
{loading ? (
Loading results for "{query}"...
) : (
)}
);
}
// 自定义Hook封装常见Effect模式
function useDocumentTitle(title) {
useEffect(() => {
const originalTitle = document.title;
document.title = title;
return () => {
document.title = originalTitle;
};
}, [title]);
}
function useWindowEvent(eventType, handler) {
useEffect(() => {
window.addEventListener(eventType, handler);
return () => {
window.removeEventListener(eventType, handler);
};
}, [eventType, handler]);
}
// 使用自定义Hook
function ProfilePage({ user }) {
// 使用封装的Effect
useDocumentTitle(`${user.name}'s Profile`);
const handleEscape = useCallback((event) => {
if (event.key === 'Escape') {
console.log('Escape pressed');
}
}, []);
useWindowEvent('keydown', handleEscape);
return Profile content...;
}
类组件生命周期方法 | useEffect等价写法 | 生活类比 |
---|---|---|
componentDidMount | useEffect(() => {…}, []) | 首次入职大扫除 |
componentDidUpdate | useEffect(() => {…}, [prop1, prop2]) | 特定条件变化时的定期清洁 |
componentWillUnmount | useEffect(() => { return () => {…} }, []) | 离职前的最终清理 |
componentDidMount + WillUnmount | useEffect(() => {…; return () => {…}}, []) | 入职配置与离职清理 |
shouldComponentUpdate | React.memo + useMemo/useCallback | 判断是否需要重新整理办公室 |
useEffect是React函数组件中处理副作用的强大魔法。它让我们能够:
记住:useEffect不是一次性事件,而是一种"同步"机制,它确保你的组件与它需要交互的外部世界保持协调一致。
通过掌握useEffect,你已经拥有了React魔法世界中最强大的咒语之一!♂️✨