您现在的位置是:网站首页 > 内存泄漏的常见模式陷阱文章详情
内存泄漏的常见模式陷阱
陈川
【
JavaScript
】
17522人已围观
6633字
内存泄漏的常见模式陷阱
内存泄漏是JavaScript开发中常见的问题,尤其在不经意间容易掉入陷阱。了解这些模式有助于避免性能下降和崩溃风险。
意外的全局变量
未声明的变量会被自动提升为全局对象的属性。浏览器环境中,这会导致变量一直存在于内存中,直到页面关闭。
function leakExample() {
leakedVar = 'This is a global variable'; // 未使用var/let/const
this.anotherLeak = 'Also global'; // 函数被当作普通函数调用时
}
严格模式可以避免这种意外:
'use strict';
function strictExample() {
leakedVar = 'Error will be thrown'; // ReferenceError
}
闭包引起的内存滞留
闭包会保留其外部函数的作用域链,不当使用会导致意外引用:
function createClosure() {
const hugeArray = new Array(1000000).fill('*');
return function() {
console.log('Closure created');
// 无意中保留了hugeArray的引用
};
}
const closureHolder = createClosure();
优化方法是只保留必要的数据:
function optimizedClosure() {
const neededData = 'essential';
const tempArray = new Array(1000000).fill('*');
return function() {
console.log(neededData);
// tempArray不再被引用
};
}
定时器未清理
setInterval和setTimeout会保持回调函数的引用,直到被清除:
let counter = 0;
const intervalId = setInterval(() => {
if (counter++ > 10) {
// 忘记调用clearInterval(intervalId)
return;
}
console.log('Counting...');
}, 1000);
更安全的做法是使用清理模式:
class TimerManager {
constructor() {
this.timers = new Set();
}
setInterval(callback, delay) {
const id = setInterval(callback, delay);
this.timers.add(id);
return id;
}
clearAll() {
this.timers.forEach(id => clearInterval(id));
this.timers.clear();
}
}
DOM引用未释放
保存DOM元素的引用会导致相关内存无法回收:
const elementsCache = {};
function cacheElement(id) {
if (!elementsCache[id]) {
elementsCache[id] = document.getElementById(id);
}
return elementsCache[id];
}
// 即使从DOM移除后,elementsCache仍保持引用
使用WeakMap可以避免这个问题:
const weakCache = new WeakMap();
function cacheElementWeakly(element) {
if (!weakCache.has(element)) {
weakCache.set(element, computeExpensiveValue(element));
}
return weakCache.get(element);
}
事件监听器堆积
未移除的事件监听器是常见的内存泄漏源:
class EventLeaker {
constructor(element) {
this.element = element;
this.handleClick = this.handleClick.bind(this);
this.element.addEventListener('click', this.handleClick);
}
handleClick() {
console.log('Element clicked');
}
// 忘记实现remove方法
}
改进的方案:
class SafeEventManager {
constructor(element) {
this.element = element;
this.listeners = new Map();
}
add(event, callback) {
const handler = callback.bind(this);
this.listeners.set(callback, handler);
this.element.addEventListener(event, handler);
}
remove(event, callback) {
const handler = this.listeners.get(callback);
if (handler) {
this.element.removeEventListener(event, handler);
this.listeners.delete(callback);
}
}
clear() {
this.listeners.forEach((handler, callback, map) => {
// 需要知道事件类型,实际实现会更复杂
this.element.removeEventListener('click', handler);
});
this.listeners.clear();
}
}
缓存无限增长
未限制大小的缓存会持续消耗内存:
const unlimitedCache = {};
function getData(key) {
if (!unlimitedCache[key]) {
unlimitedCache[key] = fetchData(key); // 假设的获取数据方法
}
return unlimitedCache[key];
}
实现LRU(最近最少使用)缓存:
class LRUCache {
constructor(maxSize = 100) {
this.maxSize = maxSize;
this.cache = new Map();
}
get(key) {
if (!this.cache.has(key)) return null;
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);
} else if (this.cache.size >= this.maxSize) {
const oldestKey = this.cache.keys().next().value;
this.cache.delete(oldestKey);
}
this.cache.set(key, value);
}
}
未完成的Promise
未处理的Promise可能保持对象引用:
function fetchData() {
return new Promise((resolve) => {
const bigData = new Array(1000000).fill('*');
// 如果忘记调用resolve/reject
// bigData将一直保留在内存中
});
}
添加超时机制:
function fetchWithTimeout(url, timeout = 5000) {
return new Promise((resolve, reject) => {
const timer = setTimeout(() => {
reject(new Error('Request timeout'));
}, timeout);
fetch(url)
.then(response => {
clearTimeout(timer);
resolve(response);
})
.catch(err => {
clearTimeout(timer);
reject(err);
});
});
}
第三方库的资源管理
某些库需要显式清理:
const chart = new ChartJS(document.getElementById('chart'), {
// 配置选项
});
// 当不再需要时
function cleanup() {
chart.destroy(); // 必须调用以释放资源
document.getElementById('chart').innerHTML = ''; // 清除canvas
}
框架特定的泄漏模式
在React中,未清理的副作用是常见问题:
function LeakyComponent() {
const [data, setData] = useState(null);
useEffect(() => {
fetch('/api/data')
.then(res => res.json())
.then(setData);
// 缺少清理函数
}, []);
return <div>{JSON.stringify(data)}</div>;
}
正确的做法:
function SafeComponent() {
const [data, setData] = useState(null);
const [abortController, setAbortController] = useState(new AbortController());
useEffect(() => {
const controller = new AbortController();
setAbortController(controller);
fetch('/api/data', { signal: controller.signal })
.then(res => res.json())
.then(setData)
.catch(err => {
if (err.name !== 'AbortError') {
console.error('Fetch error:', err);
}
});
return () => controller.abort();
}, []);
return <div>{JSON.stringify(data)}</div>;
}
观察者模式的泄漏
未取消的观察者会持续接收通知:
class Observable {
constructor() {
this.observers = [];
}
subscribe(observer) {
this.observers.push(observer);
// 忘记提供取消订阅的方法
}
notify(data) {
this.observers.forEach(observer => observer(data));
}
}
实现可取消的订阅:
class SafeObservable {
constructor() {
this.observers = new Set();
}
subscribe(observer) {
this.observers.add(observer);
return {
unsubscribe: () => this.observers.delete(observer)
};
}
notify(data) {
this.observers.forEach(observer => observer(data));
}
clear() {
this.observers.clear();
}
}
工具检测方法
使用Chrome DevTools识别内存泄漏:
- 性能面板记录时间线,观察JS堆大小
- 内存面板拍摄堆快照,比较不同时间点的对象分配
- 使用分配时间线跟踪内存分配
Node.js中的检测:
const heapdump = require('heapdump');
// 在怀疑有泄漏时手动触发
process.on('SIGUSR2', () => {
const filename = `heapdump-${process.pid}-${Date.now()}.heapsnapshot`;
heapdump.writeSnapshot(filename);
});
上一篇: 移动端环境下的模式调整