您现在的位置是:网站首页 > 内存泄漏的常见模式陷阱文章详情

内存泄漏的常见模式陷阱

内存泄漏的常见模式陷阱

内存泄漏是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识别内存泄漏:

  1. 性能面板记录时间线,观察JS堆大小
  2. 内存面板拍摄堆快照,比较不同时间点的对象分配
  3. 使用分配时间线跟踪内存分配

Node.js中的检测:

const heapdump = require('heapdump');

// 在怀疑有泄漏时手动触发
process.on('SIGUSR2', () => {
  const filename = `heapdump-${process.pid}-${Date.now()}.heapsnapshot`;
  heapdump.writeSnapshot(filename);
});

我的名片

网名:~川~

岗位:console.log 调试员

坐标:重庆市-九龙坡区

邮箱:cc@qdcc.cn

沙漏人生

站点信息

  • 建站时间:2013/03/16
  • 本站运行
  • 文章数量
  • 总访问量
微信公众号
每次关注
都是向财富自由迈进的一步