Vue 事件绑定机制

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

一、架构概览

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

  • 原生事件绑定

    通过 @click 或 v-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 等修饰符。
  • 调用 updateListeners → add → target.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,实现跨组件通信。

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

相关推荐
gc_22993 小时前
Vue框架学习(1:环境搭建)
vue.js
救救孩子把3 小时前
AI面试八股文-21_21-多模态Agent面试题-Vision-Language-Agent
人工智能·面试·职场和发展·llava·多模态agent
原则猫4 小时前
UDP 到底怎么实现可靠的
前端
Java后端的Ai之路4 小时前
14、ReactJS - 路由(Router)
前端·react.js·状态模式
裕晟资质规划5 小时前
三类涉密资质延续时限差异分析:军工60日与集成印制3个月的边界
java·服务器·前端·网络·安全
ShineWinsu6 小时前
对于JavaScript:JavaScript基础语法的解析
开发语言·前端·javascript·面试·web·基础语法·循环
月夏6 小时前
Vue3 转 React:18 项对照表 + 5 个必踩的坑(附 7 天上手路线)
前端
广州华水科技6 小时前
单北斗GNSS位移监测系统的应用与维护解析
前端
救救孩子把6 小时前
AI面试八股文-13_12-AI 框架与运维面试题
运维·人工智能·面试
梦曦i6 小时前
uni-storage 0.1.0 发布:TTL过期管理+多实例隔离
前端·storage·uni-app x