Appearance
内存泄漏预防
第一部分:清理模式
1.1 useEffect清理
javascript
// 基本清理模式
function Component() {
useEffect(() => {
// 设置
const resource = setupResource();
// 清理
return () => {
cleanupResource(resource);
};
}, []);
}
// 定时器清理
function TimerCleanup() {
useEffect(() => {
const timerId = setInterval(() => {
console.log('tick');
}, 1000);
return () => clearInterval(timerId);
}, []);
}
// 多个定时器
function MultipleTimers() {
useEffect(() => {
const timer1 = setInterval(() => {}, 1000);
const timer2 = setTimeout(() => {}, 5000);
const rafId = requestAnimationFrame(() => {});
return () => {
clearInterval(timer1);
clearTimeout(timer2);
cancelAnimationFrame(rafId);
};
}, []);
}
// 事件监听器清理
function EventListenerCleanup() {
useEffect(() => {
const handleResize = () => console.log('resize');
const handleScroll = () => console.log('scroll');
window.addEventListener('resize', handleResize);
window.addEventListener('scroll', handleScroll);
return () => {
window.removeEventListener('resize', handleResize);
window.removeEventListener('scroll', handleScroll);
};
}, []);
}
// WebSocket清理
function WebSocketCleanup({ url }) {
useEffect(() => {
const ws = new WebSocket(url);
ws.onopen = () => console.log('connected');
ws.onmessage = (event) => console.log(event.data);
ws.onerror = (error) => console.error(error);
return () => {
ws.close();
};
}, [url]);
}
// IntersectionObserver清理
function ObserverCleanup() {
const ref = useRef();
useEffect(() => {
const observer = new IntersectionObserver((entries) => {
console.log('intersection', entries);
});
if (ref.current) {
observer.observe(ref.current);
}
return () => {
observer.disconnect();
};
}, []);
return <div ref={ref}>Content</div>;
}
// MutationObserver清理
function MutationCleanup() {
useEffect(() => {
const observer = new MutationObserver((mutations) => {
console.log('mutations', mutations);
});
observer.observe(document.body, {
childList: true,
subtree: true
});
return () => {
observer.disconnect();
};
}, []);
}1.2 AbortController模式
javascript
// 取消fetch请求
function FetchWithAbort({ url }) {
const [data, setData] = useState(null);
useEffect(() => {
const controller = new AbortController();
fetch(url, { signal: controller.signal })
.then(res => res.json())
.then(setData)
.catch(error => {
if (error.name === 'AbortError') {
console.log('Fetch aborted');
} else {
console.error('Fetch error:', error);
}
});
return () => {
controller.abort();
};
}, [url]);
return <div>{data}</div>;
}
// 多个请求
function MultipleFetchCleanup() {
useEffect(() => {
const controllers = [];
const urls = ['/api/users', '/api/posts', '/api/comments'];
urls.forEach(url => {
const controller = new AbortController();
controllers.push(controller);
fetch(url, { signal: controller.signal })
.then(res => res.json())
.then(data => console.log(data))
.catch(err => {
if (err.name !== 'AbortError') {
console.error(err);
}
});
});
return () => {
controllers.forEach(controller => controller.abort());
};
}, []);
}
// axios取消
function AxiosCleanup() {
useEffect(() => {
const source = axios.CancelToken.source();
axios.get('/api/data', {
cancelToken: source.token
})
.then(response => console.log(response.data))
.catch(error => {
if (axios.isCancel(error)) {
console.log('Request cancelled');
}
});
return () => {
source.cancel('Component unmounted');
};
}, []);
}1.3 自定义Hook清理
javascript
// useEventListener Hook
function useEventListener(eventName, handler, element = window) {
const savedHandler = useRef(handler);
useEffect(() => {
savedHandler.current = handler;
}, [handler]);
useEffect(() => {
const eventListener = (event) => savedHandler.current(event);
element.addEventListener(eventName, eventListener);
return () => {
element.removeEventListener(eventName, eventListener);
};
}, [eventName, element]);
}
// useInterval Hook
function useInterval(callback, delay) {
const savedCallback = useRef(callback);
useEffect(() => {
savedCallback.current = callback;
}, [callback]);
useEffect(() => {
if (delay === null) return;
const tick = () => savedCallback.current();
const id = setInterval(tick, delay);
return () => clearInterval(id);
}, [delay]);
}
// useWebSocket Hook
function useWebSocket(url) {
const [data, setData] = useState(null);
const wsRef = useRef(null);
useEffect(() => {
const ws = new WebSocket(url);
wsRef.current = ws;
ws.onmessage = (event) => {
setData(JSON.parse(event.data));
};
ws.onerror = (error) => {
console.error('WebSocket error:', error);
};
return () => {
if (wsRef.current) {
wsRef.current.close();
wsRef.current = null;
}
};
}, [url]);
return data;
}
// useObserver Hook
function useObserver(callback, options) {
const elementRef = useRef();
useEffect(() => {
const observer = new IntersectionObserver(callback, options);
if (elementRef.current) {
observer.observe(elementRef.current);
}
return () => {
observer.disconnect();
};
}, [callback, options]);
return elementRef;
}第二部分:最佳实践
2.1 状态管理
javascript
// ✅ 限制状态大小
function BoundedState() {
const MAX_ITEMS = 100;
const [items, setItems] = useState([]);
const addItem = (item) => {
setItems(prev => {
const newItems = [...prev, item];
// 限制数组大小
if (newItems.length > MAX_ITEMS) {
return newItems.slice(-MAX_ITEMS);
}
return newItems;
});
};
return <button onClick={() => addItem({ id: Date.now() })}>Add</button>;
}
// ✅ 清理过期数据
function CleanupOldData() {
const [cache, setCache] = useState(new Map());
useEffect(() => {
const interval = setInterval(() => {
setCache(prev => {
const now = Date.now();
const newCache = new Map();
for (const [key, value] of prev) {
// 保留1小时内的数据
if (now - value.timestamp < 3600000) {
newCache.set(key, value);
}
}
return newCache;
});
}, 60000); // 每分钟清理一次
return () => clearInterval(interval);
}, []);
}
// ✅ 使用WeakMap
function UseWeakMap() {
// WeakMap的键是弱引用,对象被GC时自动移除
const cache = useRef(new WeakMap());
const getCachedData = (obj) => {
if (cache.current.has(obj)) {
return cache.current.get(obj);
}
const data = processObject(obj);
cache.current.set(obj, data);
return data;
};
// obj被GC时,cache中的条目也会被移除
}
// ✅ 使用WeakSet
function UseWeakSet() {
const processedItems = useRef(new WeakSet());
const processItem = (item) => {
if (processedItems.current.has(item)) {
return; // 已处理
}
doProcess(item);
processedItems.current.add(item);
};
}2.2 引用管理
javascript
// ✅ 及时释放引用
function ReleaseReferences() {
const heavyDataRef = useRef(null);
useEffect(() => {
// 加载大对象
heavyDataRef.current = loadHeavyData();
return () => {
// 释放引用
heavyDataRef.current = null;
};
}, []);
}
// ✅ 避免循环引用
function AvoidCircularRefs() {
const objA = { name: 'A' };
const objB = { name: 'B' };
// ❌ 循环引用
objA.ref = objB;
objB.ref = objA;
// ✅ 使用WeakMap打破循环
const refs = new WeakMap();
refs.set(objA, objB);
refs.set(objB, objA);
}
// ✅ 清理DOM引用
function DOMRefCleanup() {
const elementRef = useRef();
useEffect(() => {
const element = document.getElementById('my-element');
elementRef.current = element;
return () => {
elementRef.current = null; // 清理引用
};
}, []);
}2.3 缓存策略
javascript
// ✅ LRU缓存(最近最少使用)
class LRUCache {
constructor(capacity) {
this.capacity = capacity;
this.cache = new Map();
}
get(key) {
if (!this.cache.has(key)) return undefined;
const value = this.cache.get(key);
// 移到最后(最近使用)
this.cache.delete(key);
this.cache.set(key, value);
return value;
}
set(key, value) {
if (this.cache.has(key)) {
this.cache.delete(key);
}
this.cache.set(key, value);
// 超出容量,删除最老的
if (this.cache.size > this.capacity) {
const firstKey = this.cache.keys().next().value;
this.cache.delete(firstKey);
}
}
}
// React使用
function useLRUCache(capacity) {
const cache = useRef(new LRUCache(capacity));
return cache.current;
}
// ✅ 时间限制缓存
class TTLCache {
constructor(ttl = 60000) {
this.ttl = ttl;
this.cache = new Map();
}
set(key, value) {
this.cache.set(key, {
value,
expiry: Date.now() + this.ttl
});
}
get(key) {
const item = this.cache.get(key);
if (!item) return undefined;
if (Date.now() > item.expiry) {
this.cache.delete(key);
return undefined;
}
return item.value;
}
cleanup() {
const now = Date.now();
for (const [key, item] of this.cache) {
if (now > item.expiry) {
this.cache.delete(key);
}
}
}
}
// 定期清理
function useTTLCache(ttl) {
const cache = useRef(new TTLCache(ttl));
useEffect(() => {
const interval = setInterval(() => {
cache.current.cleanup();
}, 60000); // 每分钟清理一次
return () => clearInterval(interval);
}, []);
return cache.current;
}第二部分:React最佳实践
2.1 Hook使用规范
javascript
// ✅ 完整的useEffect清理
function ProperCleanup() {
const [data, setData] = useState(null);
useEffect(() => {
let isMounted = true;
fetchData().then(result => {
if (isMounted) {
setData(result);
}
});
return () => {
isMounted = false;
};
}, []);
}
// ✅ useCallback稳定引用
function StableCallbacks() {
const [items, setItems] = useState([]);
const addItem = useCallback((item) => {
setItems(prev => [...prev, item]);
}, []);
const removeItem = useCallback((id) => {
setItems(prev => prev.filter(item => item.id !== id));
}, []);
return { addItem, removeItem };
}
// ✅ useRef避免闭包陷阱
function AvoidClosureTrap({ value }) {
const valueRef = useRef(value);
useEffect(() => {
valueRef.current = value;
});
useEffect(() => {
const interval = setInterval(() => {
console.log(valueRef.current); // 总是最新值
}, 1000);
return () => clearInterval(interval);
}, []); // 空依赖,不会因value变化重新创建
}
// ✅ 条件清理
function ConditionalCleanup({ enabled }) {
useEffect(() => {
if (!enabled) return;
const subscription = subscribe();
return () => {
subscription.unsubscribe();
};
}, [enabled]);
}2.2 组件设计
javascript
// ✅ 分离关注点
function SeparatedConcerns() {
// 数据获取
const data = useData();
// UI渲染
return <Display data={data} />;
}
function useData() {
const [data, setData] = useState(null);
useEffect(() => {
const controller = new AbortController();
fetch('/api/data', { signal: controller.signal })
.then(res => res.json())
.then(setData)
.catch(err => {
if (err.name !== 'AbortError') {
console.error(err);
}
});
return () => controller.abort();
}, []);
return data;
}
// ✅ 组件卸载清理
function CleanupOnUnmount() {
const resourcesRef = useRef([]);
const createResource = () => {
const resource = initResource();
resourcesRef.current.push(resource);
return resource;
};
useEffect(() => {
return () => {
// 批量清理所有资源
resourcesRef.current.forEach(resource => {
resource.cleanup();
});
resourcesRef.current = [];
};
}, []);
return <div>Component</div>;
}
// ✅ 条件渲染清理
function ConditionalRender({ show }) {
return (
<div>
{show && <HeavyComponent />}
</div>
);
// show为false时,HeavyComponent卸载并清理
}2.3 全局状态管理
javascript
// ✅ Redux清理
function ReduxCleanup() {
const dispatch = useDispatch();
useEffect(() => {
// 订阅
const unsubscribe = store.subscribe(() => {
console.log('state changed');
});
return () => {
unsubscribe();
};
}, []);
useEffect(() => {
return () => {
// 组件卸载时清理状态
dispatch({ type: 'CLEANUP' });
};
}, [dispatch]);
}
// ✅ Context清理
const DataContext = React.createContext();
function DataProvider({ children }) {
const [data, setData] = useState({});
const subscribersRef = useRef(new Set());
const subscribe = useCallback((callback) => {
subscribersRef.current.add(callback);
return () => {
subscribersRef.current.delete(callback);
};
}, []);
useEffect(() => {
return () => {
// 清理所有订阅
subscribersRef.current.clear();
};
}, []);
return (
<DataContext.Provider value={{ data, subscribe }}>
{children}
</DataContext.Provider>
);
}
// ✅ 状态持久化清理
function PersistentState() {
const [state, setState] = useState(() => {
const saved = localStorage.getItem('myState');
return saved ? JSON.parse(saved) : {};
});
useEffect(() => {
localStorage.setItem('myState', JSON.stringify(state));
}, [state]);
useEffect(() => {
return () => {
// 可选:卸载时清理
localStorage.removeItem('myState');
};
}, []);
}第三部分:预防性编码
3.1 ESLint规则
javascript
// .eslintrc.js
module.exports = {
extends: ['react-app', 'react-hooks'],
rules: {
'react-hooks/exhaustive-deps': 'warn',
'react-hooks/rules-of-hooks': 'error'
},
plugins: ['react-hooks']
};
// 自定义规则检测泄漏
module.exports = {
rules: {
'no-uncleaned-effects': {
meta: {
type: 'problem',
docs: {
description: 'Enforce cleanup in useEffect'
}
},
create(context) {
return {
CallExpression(node) {
if (node.callee.name === 'useEffect') {
const callback = node.arguments[0];
// 检查是否有return语句
const hasReturn = callback.body.body.some(
statement => statement.type === 'ReturnStatement'
);
if (!hasReturn) {
context.report({
node,
message: 'useEffect should return a cleanup function'
});
}
}
}
};
}
}
}
};3.2 TypeScript类型安全
typescript
// 类型化的清理函数
type CleanupFunction = () => void;
function useResource(): CleanupFunction {
useEffect(() => {
const resource = createResource();
return () => {
resource.destroy();
};
}, []);
return () => {
// 额外的清理逻辑
};
}
// 强制清理
interface SubscriptionOptions {
onData: (data: any) => void;
cleanup: CleanupFunction; // 必须提供cleanup
}
function useSubscription(options: SubscriptionOptions) {
useEffect(() => {
const subscription = subscribe(options.onData);
return () => {
subscription.unsubscribe();
options.cleanup(); // 强制调用
};
}, [options]);
}3.3 测试覆盖
javascript
// 测试清理逻辑
describe('Component cleanup', () => {
it('should cleanup timer on unmount', () => {
jest.useFakeTimers();
const { unmount } = render(<TimerComponent />);
// 验证定时器启动
expect(setInterval).toHaveBeenCalled();
unmount();
// 验证定时器清理
expect(clearInterval).toHaveBeenCalled();
jest.useRealTimers();
});
it('should cleanup event listener on unmount', () => {
const addEventListenerSpy = jest.spyOn(window, 'addEventListener');
const removeEventListenerSpy = jest.spyOn(window, 'removeEventListener');
const { unmount } = render(<ListenerComponent />);
expect(addEventListenerSpy).toHaveBeenCalledWith('resize', expect.any(Function));
unmount();
expect(removeEventListenerSpy).toHaveBeenCalledWith('resize', expect.any(Function));
});
});
// E2E内存测试
// test/memory.spec.js
const puppeteer = require('puppeteer');
describe('Memory leak tests', () => {
let browser, page;
beforeAll(async () => {
browser = await puppeteer.launch({ args: ['--expose-gc'] });
});
beforeEach(async () => {
page = await browser.newPage();
});
afterEach(async () => {
await page.close();
});
afterAll(async () => {
await browser.close();
});
it('should not leak memory on modal open/close', async () => {
await page.goto('http://localhost:3000');
const getMemory = () => page.evaluate(() => performance.memory.usedJSHeapSize);
const initial = await getMemory();
// 打开关闭模态框50次
for (let i = 0; i < 50; i++) {
await page.click('#open-modal');
await page.waitForSelector('.modal');
await page.click('#close-modal');
await page.waitForFunction(() => !document.querySelector('.modal'));
}
// 强制GC
await page.evaluate(() => {
if (window.gc) window.gc();
});
const final = await getMemory();
const growth = ((final - initial) / initial) * 100;
console.log('Memory growth:', growth.toFixed(2), '%');
// 允许20%的增长
expect(growth).toBeLessThan(20);
});
});注意事项
1. 清理的完整性
javascript
// ✅ 完整的清理检查清单
function CompleteCleanup() {
useEffect(() => {
// 1. 定时器
const timer = setInterval(() => {}, 1000);
// 2. 事件监听
const handler = () => {};
window.addEventListener('resize', handler);
// 3. 订阅
const subscription = subscribe();
// 4. WebSocket
const ws = new WebSocket('ws://...');
// 5. Observer
const observer = new IntersectionObserver(() => {});
// 6. 请求
const controller = new AbortController();
return () => {
clearInterval(timer);
window.removeEventListener('resize', handler);
subscription.unsubscribe();
ws.close();
observer.disconnect();
controller.abort();
};
}, []);
}2. 开发环境检查
javascript
// 严格模式检测
// StrictMode会双重调用effects
<React.StrictMode>
<App />
</React.StrictMode>
// 如果清理不当,会看到明显问题3. 生产监控
javascript
// 监控内存使用
function useMemoryMonitoring() {
useEffect(() => {
const interval = setInterval(() => {
if (performance.memory) {
const used = performance.memory.usedJSHeapSize;
const limit = performance.memory.jsHeapSizeLimit;
const percent = (used / limit) * 100;
if (percent > 90) {
// 告警
console.error('Memory usage critical:', percent.toFixed(2), '%');
sendAlert('high-memory-usage', { percent });
}
}
}, 30000); // 每30秒检查
return () => clearInterval(interval);
}, []);
}常见问题
Q1: 如何确认清理函数被调用?
A: 添加console.log或使用React DevTools。
Q2: 清理函数可以是异步的吗?
A: 不可以,必须是同步函数。
Q3: WeakMap和Map的区别?
A: WeakMap的键是弱引用,对象被GC时自动清理。
Q4: 如何预防第三方库泄漏?
A: 阅读文档,确保正确调用销毁方法。
Q5: 状态过大会泄漏吗?
A: 不算泄漏,但应限制大小避免内存压力。
Q6: 开发环境比生产环境更容易泄漏吗?
A: StrictMode会暴露问题,但泄漏本身无差异。
Q7: 如何测试清理逻辑?
A: 编写unmount测试,验证清理函数调用。
Q8: 闭包一定会泄漏吗?
A: 不一定,但要注意闭包引用的对象大小。
Q9: HOC和Hook哪个更容易泄漏?
A: 都可能泄漏,关键是正确的清理逻辑。
Q10: React 19对泄漏预防有帮助吗?
A: 更好的开发工具和严格检查有助于预防。
总结
核心要点
1. 清理原则
✅ 有setup就有cleanup
✅ useEffect返回清理函数
✅ 组件卸载清理资源
✅ 及时释放引用
2. 常见场景
✅ 定时器
✅ 事件监听
✅ 订阅
✅ 网络请求
3. 预防措施
✅ ESLint规则
✅ TypeScript类型
✅ 代码审查
✅ 自动化测试最佳实践
1. 开发习惯
✅ 编写cleanup同时编写setup
✅ 使用自定义Hook封装
✅ 注意闭包引用
✅ 限制缓存大小
2. 测试策略
✅ 单元测试清理逻辑
✅ E2E测试内存增长
✅ 定期手动检查
✅ 生产监控
3. 团队规范
✅ 代码审查重点检查
✅ 文档清理要求
✅ 分享最佳实践
✅ 持续学习改进预防胜于治疗,建立良好的编码习惯和规范是避免内存泄漏的最有效方法。