Vue 事件绑定机制

Vue 将事件系统拆分为原生 DOM 事件与自定义组件事件两套正交实现,前者对接浏览器事件循环,后者基于发布--订阅模型。本文以 v-on(缩写 @)为线索,结合运行时源码路径,给出端到端的实现剖析。

一、架构概览

Vue 的事件绑定分为两条主线:

  • 原生事件绑定

    通过 @clickv-on:click 直接作用于普通 DOM 元素,最终调用浏览器的 addEventListener

  • 组件事件绑定

    通过 @click 作用于子组件标签时,实际上是父组件监听子组件的自定义事件,由子组件通过 $emit 触发,不经过 DOM。

二、原生事件绑定:从 AST 到 addEventListener

1.编译阶段

模板中的 @click="handler" 经模板编译器解析后,生成 AST,最终转化为 VNode 的 data.on = { click: handler }

2.运行时挂载

首次渲染时,patch 过程会调用 createElm,为真实 DOM 节点执行 invokeCreateHooks,其中 cbs.create 包含 updateDOMListeners(位于 src/platforms/web/runtime/modules/events.js)。

updateDOMListeners 的职责:

  • 归一化事件名,处理 IE 兼容性差异。
  • 生成包裹函数,处理 .once.passive.capture 等修饰符。
  • 调用 updateListenersaddtarget.addEventListener(type, wrappedHandler, useCapture)

3.更新阶段

当组件更新时,patch 再次调用 updateDOMListeners,通过 sameVnode 判断事件差异,按需移除旧事件并重新绑定新事件。

三、组件事件绑定:on + events + emit

1.父组件编译

<Child @click="handleClick" /> 编译后,VNode 的 componentOptions.listeners = { click: handleClick },不会出现在 DOM 属性上。

2.子组件初始化

子组件实例化时:

  • initInternalComponent 将父级 listeners 注入到 vm.$options._parentListeners
  • initEvents 创建 _events = Object.create(null) 作为事件中心。
  • _parentListeners 非空,执行 updateComponentListeners(vm, _parentListeners),内部通过 $on 注册事件:
js 复制代码
function add(event, fn) {
  vm.$on(event, fn)
}

3.手动触发

子组件内部调用 this.$emit('click', payload) 时,执行:

js 复制代码
const cbs = vm._events[event]
if (cbs) {
  cbs.forEach(cb => cb(payload))
}

整个过程与浏览器事件体系完全隔离,因此可跨层级通信,且参数可控。

四、.native:在组件根节点强制使用原生事件

<Child @click.native="handler" /> 编译为 nativeOn 而非 on,运行时由 updateDOMListeners 读取 nativeOn,流程与原生事件一致,绑定在组件根 DOM 上。

五、事件修饰符实现细节

  • .stop:包裹函数内调用 e.stopPropagation()
  • .prevent:包裹函数内调用 e.preventDefault()
  • .once:绑定后立即移除监听器,并标记 _withOnce
  • .passive:调用 addEventListener(type, fn, { passive: true })
  • .capture:第三个参数传入 useCapture: true

六、性能与内存考量

  • 原生事件由浏览器托管,Vue 仅在 VNode 销毁时执行 removeEventListener,无额外开销。
  • 组件事件存储在 JS 对象,组件销毁时统一 $off,防止内存泄漏。

结论

Vue 事件系统通过"编译期转换 + 运行时调度"实现高度抽象:

  • 原生事件:AST → VNode → patch → addEventListener,完全对齐浏览器。
  • 组件事件:父子间通过 VNode.listeners → vm.events → emit,脱离 DOM,实现跨组件通信。

理解这一分层设计,有助于在复杂场景(服务端渲染、微前端、自定义渲染器)中精准定位事件相关问题。

相关推荐
小小愿望几秒前
前端读取与导出XLSX文件实战指南(React+Ant Design)
前端
小小愿望10 分钟前
JavaScript生成随机数的全面指南:别再只会用Math.random()了!
前端
Jackson__17 分钟前
RAG究竟是什么?一文搞懂大模型的记忆增强术
前端
雲墨款哥19 分钟前
JS算法练习-Day10-判断单调数列
前端·javascript·算法
前端市界19 分钟前
前端视角: PyQt6+Vue3 跨界开发实战
前端·qt·pyqt
阅文作家助手开发团队_山神21 分钟前
第四章:Flutter自定义Engine本地依赖与打包流程
前端·flutter
JuneXcy24 分钟前
11.web api 2
前端·javascript·html
congvee24 分钟前
vue学习第3期 - 集成UI库
vue.js
zYear29 分钟前
Elpis 全栈应用框架-- 总结
前端·javascript
程序员曦曦31 分钟前
10:00开始面试,10:06就出来了,问的问题有点变态。。。
自动化测试·软件测试·功能测试·程序人生·面试·职场和发展