三、实战验证:用 Chrome DevTools 抓“幽灵”
打开 Chrome → Memory 面板 → Take heap snapshot:
打开页面,添加 3 个联系人
卡片删除所有卡片
手动触发垃圾回收(GC)
再次快照
在 Comparison 模式下你会发现:
Detached <div>:3 个脱离 DOM 的元素
Closure 或 Function:3 个对应的事件处理函数
VueComponent:3 个未被回收的组件实例
这些就是“幽灵组件”——它们已经不在页面上,却依然占据内存。
四、正确写法:绑定与解绑必须成对出现
mounted() {
  document.addEventListener('click', this.handleClickOutside)
},
beforeDestroy() {
  // ✅ 必须解绑
  document.removeEventListener('click', this.handleClickOutside)
}
或者使用 事件选项 { once: true } 自动清理:
mounted() {
  document.addEventListener('click', this.handleClickOutside, { once: true })
}
但在轮询或持续监听场景中,手动管理仍是必须的。
五、除了事件监听,还有哪些常见内存泄漏点?
1.定时器未清理(最常见)
mounted() {
  this.timer = setInterval(() => {
    console.log(this.msg) // 引用组件实例
  }, 1000)
},
beforeDestroy() {
  // 🔴 忘记 clearInterval(this.timer)
}
同样的引用链:window → setInterval → callback → component
2. 观察者模式未退订
使用 EventBus 或自定义事件系统时:
// main.js
export const bus = new Vue()
// ComponentA.vue
mounted() {
  bus.$on('data-updated', this.handleUpdate) // 🔴 忘记 $off
}
即使 ComponentA 销毁了,bus 仍持有其 handleUpdate 方法,导致实例无法回收。
正确做法:
beforeDestroy() {
  bus.$off('data-updated', this.handleUpdate)
}
3. 闭包引用大型对象
function createWorker() {
  const hugeData = new Array(1000000).fill('leak') // 100万条数据
  return {
    process(id) {
      return hugeData[id] // 🔍 闭包引用,无法释放
    },
    cleanup() {
      hugeData.length = 0 // 手动清空
    }
  }
}
只要 process 函数存在,hugeData 就不会被回收。
4. DOM 引用未释放:
let globalRef = null
mounted() {
  globalRef = this.$el // 🔴 把 DOM 节点挂到全局变量
}
即使组件销毁,globalRef 仍指向旧 DOM,且其关联的事件、属性都无法清理。
5. WeakMap/WeakSet 使用不当
你以为 WeakMap 能自动清理?错!只有键(key)是对象时才弱引用:
const cache = new WeakMap()
mounted() {
  const key = { id: this._uid }
  cache.set(key, this.someHeavyData) // ❌ key 是局部对象,WeakMap 无效
}
正确用法是把 组件实例作为 key:
const cache = new WeakMap()
// 全局缓存,key 是组件实例,value 是计算结果
cache.set(this, expensiveResult)
// 当组件被回收,cache 中对应条目自动消失
六、主流框架如何帮我们规避?

记住:框架只能管“它知道的”事件。一旦你走出框架封装,进入原生 API,责任就回到开发者身上。
七、举一反三:三个高风险场景应对策略
第三方 SDK 回调泄漏
如地图 API、视频播放器。
解决方案:封装 SDK 实例到组件内,beforeDestroy 中调用 map.destroy() 或 player.dispose()。
WebSocket 长连接未关闭
mounted() {
  this.ws = new WebSocket('wss://live-data')
},
beforeDestroy() {
  this.ws.close() // 🔍 必须关闭,否则连接和回调都驻留
}
3.Canvas/WebGL 纹理未释放
图形资源直接占用 GPU 内存。需手动 gl.deleteTexture()、canvas.remove()。
八、防御性编程 checklist
每次写可能造成泄漏的代码时,问自己:
✅ 是否有配对的“清理函数”?
✅ 引用链是否会意外延长对象生命周期?
✅ 是否可以通过 WeakMap/WeakSet 优化?
✅ 能否用 { once: true } 或 AbortController 自动管理?
小结
内存泄漏不是“会不会发生”的问题,而是“何时爆发”的问题。它像慢性病,初期毫无征兆,等到用户投诉卡顿时,往往已经积重难返。
真正的前端专家,不是会写多炫酷的动画,而是能在每一行代码里看到潜在的资源生命周期。
参考文章:原文链接