useEffect 的清理机制是 React 中处理副作用的重要部分,它可以防止内存泄漏和避免出现意外行为。
1. 为什么需要清理?
在以下情况下需要清理:
- 取消订阅(例如 WebSocket 连接)
- 清除定时器
- 取消网络请求
- 移除事件监听器
如果不进行清理,可能会导致:
- 内存泄漏
- 组件卸载后仍在执行的副作用
- 过时的状态更新
- 重复的事件监听
2. 基本清理示例
import React, { useState, useEffect } from 'react';
function Timer() {
const [count, setCount] = useState(0);
useEffect(() => {
// 设置定时器
const timer = setInterval(() => {
setCount(c => c + 1);
}, 1000);
// 清理函数
return () => {
clearInterval(timer); // 清除定时器
};
}, []); // 空依赖数组,只在组件挂载时执行
return <div>Count: {count}</div>;
}
3. 事件监听器清理
import React, { useState, useEffect } from 'react';
function WindowSizeTracker() {
const [windowSize, setWindowSize] = useState({
width: window.innerWidth,
height: window.innerHeight
});
useEffect(() => {
// 定义处理函数
const handleResize = () => {
setWindowSize({
width: window.innerWidth,
height: window.innerHeight
});
};
// 添加事件监听器
window.addEventListener('resize', handleResize);
// 清理函数
return () => {
window.removeEventListener('resize', handleResize);
};
}, []); // 空依赖数组,只在组件挂载时执行
return (
<div>
Window size: {windowSize.width} x {windowSize.height}
</div>
);
}
4. 异步操作的清理
import React, { useState, useEffect } from 'react';
function UserData({ userId }) {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
let isMounted = true; // 标记组件是否已挂载
const fetchUser = async () => {
try {
setLoading(true);
const response = await fetch(`/api/users/${userId}`);
const data = await response.json();
// 只在组件仍然挂载时更新状态
if (isMounted) {
setUser(data);
setLoading(false);
}
} catch (error) {
if (isMounted) {
console.error('Error fetching user:', error);
setLoading(false);
}
}
};
fetchUser();
// 清理函数
return () => {
isMounted = false; // 标记组件已卸载
};
}, [userId]); // 依赖于 userId
if (loading) return <div>Loading...</div>;
if (!user) return <div>No user data</div>;
return (
<div>
<h2>{user.name}</h2>
<p>{user.email}</p>
</div>
);
}
5. WebSocket 连接的清理
import React, { useState, useEffect } from 'react';
function ChatRoom({ roomId }) {
const [messages, setMessages] = useState([]);
useEffect(() => {
const ws = new WebSocket(`ws://example.com/chat/${roomId}`);
ws.onmessage = (event) => {
const message = JSON.parse(event.data);
setMessages(prev => [...prev, message]);
};
// 清理函数
return () => {
ws.close(); // 关闭 WebSocket 连接
};
}, [roomId]);
return (
<div>
{messages.map((msg, index) => (
<div key={index}>{msg.text}</div>
))}
</div>
);
}
6. 订阅模式的清理
import React, { useEffect } from 'react';
import { eventEmitter } from './eventEmitter';
function NotificationListener() {
useEffect(() => {
const handleNotification = (message) => {
console.log('New notification:', message);
};
// 订阅事件
eventEmitter.on('notification', handleNotification);
// 清理函数
return () => {
// 取消订阅
eventEmitter.off('notification', handleNotification);
};
}, []);
return <div>Listening for notifications...</div>;
}
清理机制的重要注意事项:
-
执行时机
- 组件卸载时
- 依赖项改变导致效果重新执行时
- 组件重新渲染时(如果没有依赖项)
-
常见错误
useEffect(() => {
// ❌ 错误:清理函数中使用了过时的闭包值
const timer = setInterval(() => {
console.log(count); // 可能使用的是旧的 count 值
}, 1000);
return () => clearInterval(timer);
}, []); // 依赖项数组为空
// ✅ 正确:使用函数更新形式
useEffect(() => {
const timer = setInterval(() => {
setCount(c => c + 1); // 使用函数更新形式
}, 1000);
return () => clearInterval(timer);
}, []);
- 性能优化
useEffect(() => {
const handleScroll = () => {
// 处理滚动事件
};
// 使用防抖优化滚动事件监听
const debouncedHandler = debounce(handleScroll, 100);
window.addEventListener('scroll', debouncedHandler);
return () => {
window.removeEventListener('scroll', debouncedHandler);
// 取消未执行的防抖函数
debouncedHandler.cancel();
};
}, []);
正确使用清理机制可以防止内存泄漏和意外行为,使得组件更加健壮和可靠。在编写 useEffect 时,应该始终考虑是否需要清理,以及如何正确实现清理功能。