Vue2 中什么时候需要用 addEventListener 而不是 v-on?
不少刚接触 Vue2 的同学,在处理事件时会疑惑:明明有 v-on 指令,为啥还要学 addEventListener?什么时候得用原生事件绑定?绑定后又该咋避免内存泄漏?今天就围绕 Vue2 里的 addEventListener 把这些问题掰碎了讲。
首先得明白 v-on 和 addEventListener 的本质区别,v-on 是 Vue 封装的指令,处理 DOM 事件时会做事件委托(比如列表里的按钮,Vue 会在父元素上绑一次事件,利用冒泡减少性能消耗),还能处理组件间的自定义事件(子组件 $emit 触发父组件方法),但碰到这几种情况,v-on 就不够用了:
- 和第三方库交互时:比如集成地图 SDK(百度/高德地图)、视频播放器(Video.js),这些库的 DOM 元素不是 Vue 直接管理的,得用原生事件绑定才能监听到它们的点击、缩放等事件。
- 给非 Vue 渲染的 DOM 绑事件:比如页面里有个通过原生 JS 动态创建的弹窗,或者浏览器插件注入的元素,Vue 的 v-on 够不着,只能用 addEventListener 硬绑。
- 需要控制事件阶段:addEventListener 第三个参数可以设置
useCapture
(是否在捕获阶段触发事件),v-on 只能在冒泡阶段处理事件,比如要拦截子元素的点击事件,阻止它冒泡到父元素,就需要在捕获阶段绑事件。 - 做全局事件监听:比如监听浏览器的
resize
、scroll
,或者给document
/window
绑自定义事件实现跨组件通信,这些场景用 addEventListener 更灵活。
Vue2 里怎么正确使用 addEventListener 绑定事件?
核心逻辑是“在 DOM 渲染完成后绑定,用组件方法当回调,方便后续解绑”,具体步骤看例子:
步骤 1:选对生命周期钩子
Vue 组件的 mounted
钩子执行时,DOM 已经渲染完成,所以绑定事件一般写在 mounted
里,比如给页面上的 canvas 绑点击事件:
export default { mounted() { // 通过 $refs 获取 DOM(前提是给 canvas 加 ref="canvas") this.$refs.canvas.addEventListener('click', this.handleCanvasClick) }, methods: { handleCanvasClick(e) { console.log('canvas 被点击了', e) // 这里的 this 指向 Vue 实例,能直接访问 data、props 等 } } }
要是 DOM 是动态生成的(v-for 渲染的列表),得确保绑事件时元素已经存在,如果拿不准,也可以用 this.$nextTick
延迟执行绑定逻辑,保证 DOM 更新完成。
步骤 2:处理 this 指向(可选)
addEventListener 的回调函数里,this
默认指向绑定事件的 DOM 元素(比如上面的 canvas),但 Vue 方法里的 this
默认指向组件实例,所以直接把组件方法(如 handleCanvasClick
)当回调,this
就还是 Vue 实例,不用额外 bind 或用箭头函数。
但如果用匿名函数当回调(addEventListener('click', () => { ... })
),this
虽然能指向 Vue 实例,但后续解绑会失败(因为匿名函数没法精准匹配),所以一定要用具名函数当回调!
addEventListener 绑定的事件怎么解绑?为啥必须解绑?
不解绑会出大问题:组件销毁后,事件监听器还挂在 DOM 上,导致内存泄漏(浏览器没法回收这些资源),页面越用越卡,所以解绑是必做步骤,关键是“在组件销毁前解绑,且参数和绑定完全一致”。
步骤:在 beforeDestroy 钩子解绑
Vue 的 beforeDestroy
钩子会在组件销毁前执行,这里写解绑逻辑最安全,延续上面 canvas 的例子:
export default { mounted() { this.$refs.canvas.addEventListener('click', this.handleCanvasClick) }, methods: { handleCanvasClick(e) { ... } }, beforeDestroy() { this.$refs.canvas.removeEventListener('click', this.handleCanvasClick) } }
注意这几点:
- 事件名、回调函数、
useCapture
(第三个参数)必须和绑定时候完全一样,比如绑定的时候用了捕获阶段(addEventListener('click', fn, true)
),解绑时第三个参数也得传true
。 - 回调必须是同一个函数引用,如果绑定用的是匿名函数,解绑时找不到对应的函数,等于白写,所以一定要用组件 methods 里的具名函数。
- 如果绑定的是
window
/document
这类全局对象,解绑时也要用同样的对象,比如绑了window.addEventListener('resize', fn)
,解绑得用window.removeEventListener(...)
。
addEventListener 和 v-on 处理事件有啥本质区别?
很多同学觉得“都是绑事件,用哪个都行”,但两者底层逻辑差很多,选错了容易踩坑:
对比维度 | addEventListener | v-on |
---|---|---|
底层实现 | 原生 JS 方法,直接绑在 DOM 元素上 | Vue 封装的指令,DOM 事件会做事件委托(减少重复绑定) |
事件类型 | 只能处理DOM 原生事件(click、scroll 等) | 既能处理 DOM 原生事件,也能处理组件自定义事件(子组件 $emit 触发) |
性能表现 | 给大量元素绑同类事件时(比如列表里每个按钮绑 click),会重复绑定,性能差 | DOM 事件用事件委托,只在父元素绑一次,性能更优 |
事件阶段控制 | 能设置捕获阶段(useCapture: true) | 只能在冒泡阶段触发事件,无法控制阶段 |
解绑逻辑 | 必须手动调用 removeEventListener,否则内存泄漏 | Vue 自动管理解绑(组件销毁时,v-on 绑的事件会自动移除) |
简单说:日常写 Vue 组件,优先用 v-on;碰到第三方库、全局事件、捕获阶段需求,再用 addEventListener。
在 Vue2 组件通信里,addEventListener 能怎么玩?
Vue2 里组件通信方式很多(props/$emit、事件总线、Vuex 等),但 addEventListener 能实现更“原生”的跨组件通信,适合简单场景,举个兄弟组件通信的例子:
场景:组件 A 点击按钮,组件 B 接收数据
思路是用 document
当“事件中转站”,A 触发自定义事件,B 监听这个事件。
// 组件 A(触发方) <template><button @click="sendMsg">发消息</button></template> export default { methods: { sendMsg() { // 自定义事件名:brother-event,传参用 detail const event = new CustomEvent('brother-event', { detail: { msg: '吃饭了没?' } }) document.dispatchEvent(event) // 触发全局事件 } } } <p>// 组件 B(接收方) export default { mounted() { // 监听 document 上的 brother-event 事件 document.addEventListener('brother-event', this.handleMsg) }, methods: { handleMsg(e) { console.log('收到消息:', e.detail.msg) // 吃饭了没? } }, beforeDestroy() { // 销毁前解绑,避免内存泄漏 document.removeEventListener('brother-event', this.handleMsg) } }
这种方式不用依赖 Vuex 或事件总线插件,纯原生 JS + Vue 生命周期就能实现跨组件通信,适合小项目里简单的交互逻辑。
用 addEventListener 常见的坑有哪些?怎么避?
踩过这些坑的同学肯定懂那种“代码没报错,但事件不触发/重复触发/内存爆炸”的绝望,总结几个高频坑和解决方案:
坑 1:忘记解绑,内存泄漏
表现:组件销毁后,事件还在触发,浏览器内存占用越来越高。
原因:addEventListener 绑的事件不会被 Vue 自动销毁,必须手动 removeEventListener。
解法:在 beforeDestroy
(或 unmounted
,Vue3 里)里写解绑逻辑,且参数和绑定完全一致。
坑 2:事件重复绑定
表现:点一次按钮,回调执行多次(mounted 里的绑定逻辑被多次执行)。
原因:绑定逻辑所在的钩子(如 mounted)被多次触发(比如组件用 keep-alive 缓存,activated 会重复执行,但 mounted 只执行一次?不,keep-alive 时 mounted 只执行一次,activated 每次激活执行,所以如果在 activated 里绑事件,没在 deactivated 里解绑,就会重复绑)。
解法:确保“绑定 - 解绑”成对出现,比如在 activated 里绑,deactivated 里解绑;或者在绑定前判断是否已绑定(用标志位,isEventBound
)。
坑 3:事件类型拼写错误
表现:事件完全不触发,控制台没报错。
原因:把 click
写成 clik
,把 resize
写成 rezie
等。
解法:绑定前仔细检查事件名,或者把事件名存在变量里(const eventType = 'click'
),避免手误。
坑 4:捕获阶段和冒泡阶段搞混
表现:想拦截子元素事件,结果没生效;或者解绑时因为阶段参数不一致,解绑失败。
原因:addEventListener 第三个参数 useCapture
控制事件阶段,绑定和解绑时参数必须一致,比如绑定用了 true
(捕获阶段),解绑时没传 true
,就会解绑失败。
解法:明确自己需要的事件阶段,绑定和解绑时第三个参数保持一致。
坑 5:动态 DOM 绑定失败
表现:给 v-for 生成的元素绑事件,结果拿不到 DOM。
原因:v-for 渲染的 DOM 是动态生成的,mounted 执行时可能还没渲染完(尤其是异步数据渲染的列表)。
解法:用 this.$nextTick
延迟绑定,确保 DOM 渲染完成;或者用 MutationObserver 监听 DOM 变化后再绑事件。
把这些坑避开,addEventListener 在 Vue2 里就能用得顺风顺水啦~
Vue2 里的 addEventListener 是 v-on 的“补位选手”——日常开发优先用 v-on 省心,但碰到第三方库、全局事件、捕获阶段这些特殊场景,就得靠 addEventListener 上场,核心是掌握绑定时机、解绑逻辑、场景边界,再避开那些常见的小坑,就能用它解决很多复杂交互问题啦~要是你在实际开发中还有其他疑问,评论区随时聊~
版权声明
本文仅代表作者观点,不代表Code前端网立场。
本文系作者Code前端网发表,如需转载,请注明页面地址。
发表评论:
◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。