事件监听器绑定闭包易致内存泄漏,主因是闭包捕获冗余对象且监听器未及时移除;应复用函数引用、提取必要参数、同步清理,并确保第三方库调用销毁方法。

事件监听器绑定闭包是最常见、也最容易被忽视的内存泄漏源头。问题不在于用了闭包,而在于闭包捕获了不该长期持有的对象,且监听器本身没被及时移除。
监听器挂载在全局对象上,却忘了卸载
比如给 window 或 document 绑定 resize、scroll、keydown 等事件时,回调函数若用箭头函数或匿名函数直接访问组件实例、DOM 节点或大型数据,就会形成强引用链:
- window → eventListeners → bound handler → closure → this(组件实例)→ state → 大数组 / 缓存对象
- 即使组件已卸载、DOM 节点已被 remove(),只要监听器还在,整条链就无法被 GC
- 典型写法:
document.addEventListener('click', () => this.handle())—— 每次都新建闭包,且无法精准解绑
闭包隐式捕获整个作用域,远超实际需要
闭包会保留外层函数全部变量的引用,哪怕回调里只用到一个字段,整个组件实例或配置对象也会被拖住:
- 错误示例:在
mounted()中写element.addEventListener('click', () => console.log(this.data, this.config, this.$refs)) - 正确做法:只传必要参数,或用
useRef/useState快照保存轻量值,避免直接读this或响应式对象 - React 中推荐用
useCallback配合稳定依赖,Vue 中可用unref或解构提取字段
监听器注册与清理不同步,引用不匹配
想清理却清不掉,是因为注册和解绑用的不是同一个函数引用:
- 每次写
addEventListener('x', () => {...})都生成新函数,removeEventListener('x', () => {...})无效 - 必须复用同一函数:用具名函数、class 方法(绑定后存为实例属性),或用
AbortController(现代推荐) - React 的
useEffect清理函数里,确保返回的是同步执行的解绑逻辑,不能包裹 setTimeout 或 Promise
第三方库封装的监听器未暴露清理入口
像 lodash.throttle、ResizeObserver、IntersectionObserver 等,内部闭包可能持有你传入的回调,而你没调用其 cancel() 或 disconnect():
- 例如:
const throttled = throttle(handleResize, 100); window.addEventListener('resize', throttled)—— 忘记throttled.cancel()就会泄漏 - 使用前确认 API 是否提供销毁方法;没有的话,自己包装一层,确保能主动释放
- 对
ResizeObserver,务必在组件卸载时调用observer.disconnect()











