Vue 的
v-on(即@)本质上是对原生 DOM 事件的直接透传,凡是浏览器原生支持的事件,Vue 均可绑定。本文系统整理官方文档未重点列出、但实际开发中极具价值的各类事件,并附带修饰符组合、编译器处理机制与架构层面的深度说明。
一、编译器如何处理事件名
理解 Vue 如何处理事件名,是掌握所有事件绑定的基础。
1.1 事件名转换规则
Vue 编译器(@vue/compiler-dom)在处理 @eventName 时执行以下转换:
ruby
模板写法 → 编译产物(props key)
@click → onClick
@dblclick → onDblclick
@mouseenter → onMouseenter
@keydown.enter → onKeydown(运行时过滤 key)
@update:modelValue → onUpdate:modelValue
核心转换函数来自 @vue/shared:
javascript
import { toHandlerKey, camelize } from '@vue/shared'
toHandlerKey('click') // 'onClick'
toHandlerKey('dblclick') // 'onDblclick'
toHandlerKey(camelize('my-event')) // 'onMyEvent'
1.2 事件名大小写规则
xml
<!-- 以下三种写法在组件上等价(Vue 自动转换) -->
@myEvent="handler"
@my-event="handler"
@my_event="handler"
<!-- 原生 DOM 元素上:保持原始大小写,不做转换 -->
<div @dblclick="handler" /> <!-- ✅ 正确 -->
<div @DblClick="handler" /> <!-- ❌ 无法触发(原生事件名全小写) -->
二、鼠标事件(Mouse Events)
2.1 文档常见事件
xml
@click <!-- 单击 -->
@mouseover <!-- 鼠标悬入(含子元素冒泡) -->
@mouseout <!-- 鼠标离开(含子元素冒泡) -->
@mousemove <!-- 鼠标移动 -->
@mousedown <!-- 鼠标按下 -->
@mouseup <!-- 鼠标抬起 -->
@contextmenu <!-- 右键菜单 -->
2.2 文档未重点说明的鼠标事件
@dblclick --- 双击
ini
<div @dblclick="onDblClick">双击我</div>
csharp
function onDblClick(event: MouseEvent) {
// event.detail === 2,表示这是第二次点击
console.log('双击坐标:', event.clientX, event.clientY)
}
注意 :@click 和 @dblclick 同时绑定时,双击会先触发两次 click 再触发一次 dblclick。如需互斥,需手动用定时器处理:
javascript
let clickTimer: ReturnType<typeof setTimeout> | null = null
function handleClick() {
if (clickTimer) return // 等待期间忽略单击
clickTimer = setTimeout(() => {
clickTimer = null
doSingleClick()
}, 250)
}
function handleDblClick() {
if (clickTimer) {
clearTimeout(clickTimer)
clickTimer = null
}
doDblClick()
}
@mouseenter / @mouseleave --- 不冒泡的悬停事件
xml
<!-- mouseover/mouseout 会因子元素冒泡频繁触发 -->
<!-- mouseenter/mouseleave 只在进入/离开元素本身时触发一次 -->
<div
@mouseenter="isHovered = true"
@mouseleave="isHovered = false"
:class="{ hovered: isHovered }"
>
悬停区域(推荐用这对,而非 mouseover/mouseout)
</div>
架构建议 :绝大多数悬停交互应使用 mouseenter/mouseleave 而非 mouseover/mouseout,后者在子元素多的情况下会产生大量无意义的事件触发。
@auxclick --- 非主键点击(中键/侧键)
xml
<!-- 鼠标中键或其他非左键的点击 -->
<a @auxclick.prevent="openInNewTab(url)">中键点击新标签打开</a>
csharp
function onAuxClick(event: MouseEvent) {
if (event.button === 1) {
// button 1 = 中键
event.preventDefault() // 阻止中键默认的自动滚动
window.open(url, '_blank')
}
}
@click 的 button 属性区分左中右键
ini
<div @mousedown="handleMouseDown">多键区域</div>
javascript
function handleMouseDown(event: MouseEvent) {
switch (event.button) {
case 0: console.log('左键'); break
case 1: console.log('中键'); break
case 2: console.log('右键(通常触发 contextmenu)'); break
case 3: console.log('侧键后退'); break
case 4: console.log('侧键前进'); break
}
}
@click 的鼠标按键修饰符
xml
<!-- Vue 内置的鼠标按键修饰符(文档有但常被忽视) -->
<div @click.left="onLeft">仅左键触发</div>
<div @click.right="onRight">仅右键触发(不显示系统菜单)</div>
<div @click.middle="onMiddle">仅中键触发</div>
三、键盘事件(Keyboard Events)
3.1 基础事件
xml
@keydown <!-- 按键按下(持续触发) -->
@keyup <!-- 按键抬起 -->
@keypress <!-- ⚠️ 已废弃,不推荐使用 -->
3.2 按键修饰符完整列表
Vue 内置了以下按键别名,但文档中许多组合从未被举例:
xml
<!-- 字母数字类 -->
@keydown.enter="submit"
@keydown.tab="onTab"
@keydown.delete="onDelete" <!-- 同时匹配 Delete 和 Backspace -->
@keydown.esc="onEsc" <!-- 等价于 Escape -->
@keydown.space="onSpace"
@keydown.up="moveUp"
@keydown.down="moveDown"
@keydown.left="moveLeft"
@keydown.right="moveRight"
<!-- 功能键修饰符(可组合) -->
@keydown.ctrl.enter="submitForm" <!-- Ctrl + Enter -->
@keydown.shift.enter="newline" <!-- Shift + Enter -->
@keydown.alt.left="goBack" <!-- Alt + ← -->
@keydown.meta.k="openCommandPalette" <!-- Cmd/Win + K -->
@keydown.ctrl.shift.z="redo" <!-- Ctrl + Shift + Z -->
<!-- 直接使用 KeyboardEvent.key 值(kebab-case) -->
@keydown.page-up="scrollUp"
@keydown.page-down="scrollDown"
@keydown.home="gotoTop"
@keydown.end="gotoBottom"
@keydown.f1="showHelp"
@keydown.f11="toggleFullscreen"
<!-- 精确修饰符:只匹配完全一致的按键组合 -->
@keydown.exact.ctrl.a="selectAll" <!-- 只有 Ctrl+A,有其他键不触发 -->
3.3 自定义按键别名(Vue 2 遗留,Vue 3 移除)
Vue 3 已移除 Vue.config.keyCodes,推荐直接使用 KeyboardEvent.key 的 kebab-case 形式:
ini
<!-- Vue 3 直接使用原生 key 名 -->
@keydown.arrow-up="moveUp"
@keydown.home="gotoTop"
@keydown.escape="close"
@keydown.tab="focusNext"
3.4 IME 输入法相关事件(极重要但完全未文档化)
xml
<!-- compositionstart: 开始输入法输入(中日韩输入时) -->
<!-- compositionupdate: 输入法候选字更新 -->
<!-- compositionend: 输入法输入完成,确认上屏 -->
<input
@compositionstart="isComposing = true"
@compositionupdate="onCompositionUpdate"
@compositionend="isComposing = false; onInput($event)"
@input="!isComposing && onInput($event)"
/>
csharp
const isComposing = ref(false)
// 正确处理中文输入的搜索框
function onInput(event: Event) {
if (isComposing.value) return // 输入法选字过程中不触发搜索
const value = (event.target as HTMLInputElement).value
doSearch(value)
}
这是处理中文/日文/韩文输入时最常见的坑,v-model 内部已处理此问题,但手动监听 @input 时必须自行处理。
四、表单与输入事件(Form & Input Events)
4.1 @input vs @change 的区别
xml
<!-- @input:每次值变化立即触发(包括输入法过程中) -->
<input @input="onInput" />
<!-- @change:失去焦点且值有变化时触发(或按 Enter) -->
<input @change="onChange" />
<!-- select 元素 -->
<select @change="onSelect">
<option>A</option>
</select>
<!-- checkbox/radio -->
<input type="checkbox" @change="onCheckboxChange" />
4.2 焦点事件
xml
@focus <!-- 获得焦点(不冒泡) -->
@blur <!-- 失去焦点(不冒泡) -->
@focusin <!-- 获得焦点(冒泡,可在父元素监听子元素焦点) -->
@focusout <!-- 失去焦点(冒泡) -->
xml
<!-- 实用场景:监听整个表单区域内的焦点状态 -->
<div
@focusin="isFormActive = true"
@focusout="isFormActive = false"
>
<input placeholder="字段1" />
<input placeholder="字段2" />
</div>
focusin/focusout 会冒泡,而 focus/blur 不会------这是在父容器层面感知子元素焦点的标准方式。
4.3 @reset / @submit
xml
<!-- 原生 form 事件 -->
<form
@submit.prevent="handleSubmit"
@reset="handleReset"
>
<input type="text" />
<button type="submit">提交</button>
<button type="reset">重置</button>
</form>
4.4 @invalid --- 原生表单验证失败
xml
<!-- 原生 HTML5 表单验证失败时触发 -->
<input
type="email"
required
@invalid.prevent="showCustomError($event)"
/>
csharp
function showCustomError(event: Event) {
const input = event.target as HTMLInputElement
// 阻止浏览器默认气泡提示,使用自定义 UI
input.setCustomValidity('请输入有效的邮箱地址')
}
4.5 @select --- 文本选中
xml
<!-- 用户在 input/textarea 中选中文字时触发 -->
<textarea
@select="onTextSelect"
/>
vbnet
function onTextSelect(event: Event) {
const textarea = event.target as HTMLTextAreaElement
const selectedText = textarea.value.slice(
textarea.selectionStart!,
textarea.selectionEnd!
)
console.log('选中的文字:', selectedText)
}
五、拖拽事件(Drag & Drop Events)
这是文档中完全没有专门章节讲解的一类事件,但实现原生拖拽时必须用到。
xml
<!-- 被拖拽的元素上 -->
<div
draggable="true"
@dragstart="onDragStart" <!-- 开始拖拽 -->
@drag="onDragging" <!-- 拖拽中(持续触发) -->
@dragend="onDragEnd" <!-- 拖拽结束(无论是否成功) -->
>
拖我
</div>
<!-- 放置目标上 -->
<div
@dragenter="onDragEnter" <!-- 拖拽元素进入目标区域 -->
@dragover.prevent="onDragOver" <!-- 必须 prevent 才能触发 drop! -->
@dragleave="onDragLeave" <!-- 拖拽元素离开目标区域 -->
@drop.prevent="onDrop" <!-- 放置 -->
>
放这里
</div>
csharp
function onDragStart(event: DragEvent) {
event.dataTransfer!.setData('text/plain', 'payload-data')
event.dataTransfer!.effectAllowed = 'move' // 'copy' | 'move' | 'link'
}
function onDrop(event: DragEvent) {
const data = event.dataTransfer!.getData('text/plain')
console.log('接收到:', data)
}
@dragover.prevent 是关键 :不调用 preventDefault() 的话,@drop 事件永远不会触发。
5.1 文件拖拽上传
ini
<div
@dragover.prevent
@drop.prevent="onFileDrop"
class="upload-zone"
>
拖拽文件到此处上传
</div>
javascript
function onFileDrop(event: DragEvent) {
const files = Array.from(event.dataTransfer!.files)
files.forEach(file => {
console.log('文件名:', file.name, '大小:', file.size)
uploadFile(file)
})
}
六、剪贴板事件(Clipboard Events)
ini
<input
@cut="onCut"
@copy="onCopy"
@paste="onPaste"
/>
javascript
function onCopy(event: ClipboardEvent) {
// 修改复制的内容
event.preventDefault()
const selection = window.getSelection()?.toString() ?? ''
event.clipboardData!.setData('text/plain', `${selection} ------ 来自我的网站`)
}
function onPaste(event: ClipboardEvent) {
event.preventDefault()
const text = event.clipboardData!.getData('text/plain')
// 过滤内容后再插入
const sanitized = sanitizeHtml(text)
document.execCommand('insertText', false, sanitized)
}
// 读取粘贴的文件(如截图)
function onPasteWithFile(event: ClipboardEvent) {
const items = Array.from(event.clipboardData!.items)
const imageItem = items.find(item => item.type.startsWith('image/'))
if (imageItem) {
const file = imageItem.getAsFile()!
uploadImage(file)
}
}
七、滚动与视口事件
7.1 @scroll
xml
<!-- 元素滚动(注意:不冒泡,需绑在实际滚动的元素上) -->
<div class="scroll-container" @scroll="onScroll">
<div class="content">长内容...</div>
</div>
csharp
function onScroll(event: Event) {
const el = event.target as HTMLElement
const { scrollTop, scrollHeight, clientHeight } = el
// 判断是否滚动到底部
if (scrollTop + clientHeight >= scrollHeight - 10) {
loadMore()
}
}
7.2 @wheel --- 鼠标滚轮
xml
<!-- wheel 比 scroll 更底层,可以阻止滚动 -->
<div @wheel.prevent="onWheel">阻止此区域的页面滚动</div>
csharp
function onWheel(event: WheelEvent) {
event.deltaY // 垂直滚动量(正值向下,负值向上)
event.deltaX // 水平滚动量
event.deltaMode // 0=像素, 1=行, 2=页
}
@scroll 无法 preventDefault(),@wheel 可以------两者用途不同。
八、触摸事件(Touch Events)
xml
<div
@touchstart="onTouchStart" <!-- 手指接触屏幕 -->
@touchmove.prevent="onTouchMove" <!-- 手指移动(prevent 阻止页面滚动) -->
@touchend="onTouchEnd" <!-- 手指抬起 -->
@touchcancel="onTouchCancel" <!-- 触摸被中断(来电等) -->
>
触摸区域
</div>
csharp
let startX = 0
let startY = 0
function onTouchStart(event: TouchEvent) {
startX = event.touches[0].clientX
startY = event.touches[0].clientY
}
function onTouchEnd(event: TouchEvent) {
const endX = event.changedTouches[0].clientX
const endY = event.changedTouches[0].clientY
const dx = endX - startX
const dy = endY - startY
// 简单滑动手势识别
if (Math.abs(dx) > Math.abs(dy)) {
dx > 0 ? emit('swipe-right') : emit('swipe-left')
} else {
dy > 0 ? emit('swipe-down') : emit('swipe-up')
}
}
8.1 指针事件(Pointer Events)--- 统一鼠标+触摸+笔
ini
<!-- PointerEvent 是鼠标事件 + 触摸事件的统一抽象(推荐使用) -->
<div
@pointerdown="onPointerDown"
@pointermove="onPointerMove"
@pointerup="onPointerUp"
@pointerenter="onPointerEnter"
@pointerleave="onPointerLeave"
@pointercancel="onPointerCancel"
@gotpointercapture="onGotCapture"
@lostpointercapture="onLostCapture"
>
</div>
csharp
function onPointerDown(event: PointerEvent) {
event.pointerId // 指针 ID(多点触控时区分不同手指)
event.pointerType // 'mouse' | 'pen' | 'touch'
event.pressure // 压力值 0-1(笔/触控板支持)
event.tiltX // 笔的倾斜角
event.isPrimary // 是否是主指针(多点触控时第一个接触的手指)
// 捕获指针:即使鼠标移出元素也继续接收事件
;(event.target as HTMLElement).setPointerCapture(event.pointerId)
}
架构建议:新项目的拖拽、绘图、手势等功能优先使用 Pointer Events,而非分别处理 Mouse Events 和 Touch Events。**
九、媒体事件(Media Events)
这是文档完全没有覆盖的一类,对音视频开发至关重要。
xml
<video
ref="videoRef"
@loadstart="onLoadStart" <!-- 开始加载 -->
@loadedmetadata="onMetaLoaded" <!-- 元数据加载完成(可获取时长、尺寸) -->
@loadeddata="onDataLoaded" <!-- 当前帧数据加载完成 -->
@canplay="onCanPlay" <!-- 可以开始播放 -->
@canplaythrough="onCanPlayThrough" <!-- 预计不会缓冲中断地播放到结束 -->
@play="onPlay" <!-- 开始播放 -->
@playing="onPlaying" <!-- 在缓冲后恢复播放 -->
@pause="onPause" <!-- 暂停 -->
@ended="onEnded" <!-- 播放结束 -->
@timeupdate="onTimeUpdate" <!-- 播放进度更新(约每秒 4-66 次) -->
@seeking="onSeeking" <!-- 用户拖拽进度条中 -->
@seeked="onSeeked" <!-- 拖拽完成 -->
@waiting="onWaiting" <!-- 缓冲中,等待数据 -->
@stalled="onStalled" <!-- 浏览器尝试获取数据但暂停 -->
@error="onError" <!-- 加载/播放错误 -->
@volumechange="onVolumeChange" <!-- 音量或静音状态变化 -->
@ratechange="onRateChange" <!-- 播放速率变化 -->
@durationchange="onDurChange" <!-- 时长变化 -->
@progress="onProgress" <!-- 下载进度更新 -->
@suspend="onSuspend" <!-- 浏览器停止加载(数据已足够) -->
@abort="onAbort" <!-- 资源加载中止(非错误) -->
@emptied="onEmptied" <!-- 媒体资源被清空 -->
>
</video>
vbnet
function onTimeUpdate(event: Event) {
const video = event.target as HTMLVideoElement
const progress = (video.currentTime / video.duration) * 100
progressBar.value = progress
}
function onMetaLoaded(event: Event) {
const video = event.target as HTMLVideoElement
console.log('视频时长:', video.duration)
console.log('视频尺寸:', video.videoWidth, 'x', video.videoHeight)
}
十、窗口与文档事件(配合 useEventListener)
这类事件不能直接在 Vue 模板中用 @ 绑定 ,需要在 onMounted 中手动注册,或使用 VueUse 的 useEventListener。
javascript
import { onMounted, onUnmounted } from 'vue'
// 方式一:手动管理
onMounted(() => {
window.addEventListener('resize', onResize)
window.addEventListener('online', onOnline)
window.addEventListener('offline', onOffline)
document.addEventListener('visibilitychange', onVisibilityChange)
})
onUnmounted(() => {
window.removeEventListener('resize', onResize)
// ...
})
// 方式二:VueUse(推荐)
import { useEventListener } from '@vueuse/core'
useEventListener(window, 'resize', onResize)
useEventListener(document, 'visibilitychange', onVisibilityChange)
常见窗口级事件清单
javascript
// 视口
window.addEventListener('resize', handler) // 窗口大小变化
window.addEventListener('scroll', handler) // 窗口滚动
window.addEventListener('orientationchange', handler) // 屏幕旋转
// 网络状态
window.addEventListener('online', handler) // 网络恢复
window.addEventListener('offline', handler) // 网络断开
// 页面生命周期
window.addEventListener('load', handler) // 页面全部资源加载完毕
window.addEventListener('beforeunload', handler) // 页面即将卸载(可弹确认框)
window.addEventListener('unload', handler) // 页面卸载中
window.addEventListener('pagehide', handler) // 页面隐藏(bfcache 友好)
window.addEventListener('pageshow', handler) // 页面显示
// 历史记录
window.addEventListener('popstate', handler) // 浏览器前进/后退
window.addEventListener('hashchange', handler) // hash 变化
// 焦点
window.addEventListener('focus', handler) // 窗口获得焦点
window.addEventListener('blur', handler) // 窗口失去焦点
// 文档可见性
document.addEventListener('visibilitychange', () => {
if (document.visibilityState === 'hidden') {
pauseVideo() // 切换标签页时暂停视频
}
})
// 存储
window.addEventListener('storage', (event: StorageEvent) => {
// 跨标签页的 localStorage 变化通知
console.log(event.key, event.oldValue, event.newValue)
})
// 全屏
document.addEventListener('fullscreenchange', handler)
// 打印
window.addEventListener('beforeprint', handler)
window.addEventListener('afterprint', handler)
// 游戏手柄
window.addEventListener('gamepadconnected', handler)
window.addEventListener('gamepaddisconnected', handler)
十一、Intersection Observer 事件(元素进入视口)
这不是传统 DOM 事件,但在 Vue 中有标准的封装方式,常用于懒加载:
typescript
// 自定义指令实现 v-intersect
const vIntersect = {
mounted(el: HTMLElement, binding: DirectiveBinding) {
const observer = new IntersectionObserver(
(entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
binding.value(entry)
if (binding.modifiers.once) {
observer.unobserve(el)
}
}
})
},
{
threshold: binding.arg ? parseFloat(binding.arg as string) : 0,
rootMargin: '0px 0px -50px 0px'
}
)
observer.observe(el)
;(el as any).__intersectObserver__ = observer
},
unmounted(el: HTMLElement) {
;(el as any).__intersectObserver__?.disconnect()
}
}
// 使用
app.directive('intersect', vIntersect)
ini
<img
v-intersect.once="onVisible"
:src="placeholder"
data-src="real-image.jpg"
/>
十二、自定义组件事件
12.1 defineEmits 完整用法
typescript
// 带类型验证(运行时 + 编译时双重验证)
const emit = defineEmits<{
change: [value: string] // 单参数
update: [id: number, value: string] // 多参数
'node:select': [node: TreeNode] // 带命名空间的事件名
submit: [data: FormData, reset: () => void] // 包含回调函数参数
}>()
// 或带运行时验证的对象语法
const emit = defineEmits({
change: (value: string) => {
// 返回 false 表示验证失败,控制台输出警告
return typeof value === 'string' && value.length > 0
},
submit: null // null 表示不验证
})
12.2 v-model 多绑定(Vue 3.4+)
xml
<!-- 父组件 -->
<MyForm
v-model:title="title"
v-model:content="content"
v-model:tags="tags"
/>
c
// 子组件
const props = defineProps<{
title: string
content: string
tags: string[]
}>()
const emit = defineEmits<{
'update:title': [value: string]
'update:content': [value: string]
'update:tags': [value: string[]]
}>()
12.3 v-model 修饰符透传
xml
<!-- 父 -->
<MyInput v-model.trim.uppercase="text" />
csharp
// 子组件处理修饰符
const props = defineProps<{
modelValue: string
modelModifiers?: {
trim?: boolean
uppercase?: boolean
}
}>()
const emit = defineEmits<{
'update:modelValue': [value: string]
}>()
function onInput(event: Event) {
let value = (event.target as HTMLInputElement).value
if (props.modelModifiers?.trim) value = value.trim()
if (props.modelModifiers?.uppercase) value = value.toUpperCase()
emit('update:modelValue', value)
}
十三、修饰符组合速查表
perl
事件修饰符(可链式组合):
.stop → event.stopPropagation()
.prevent → event.preventDefault()
.self → 只有 event.target === 当前元素时触发
.capture → 使用捕获模式(从上到下,而非冒泡)
.once → 只触发一次,之后自动移除监听
.passive → 告知浏览器不调用 preventDefault(scroll 性能优化)
.native → ⚠️ Vue 3 已移除,请用 v-bind="$attrs" 透传
.exact → 精确匹配按键组合,不允许其他修饰键
实用组合:
@click.stop.prevent → 阻止冒泡 + 阻止默认行为
@scroll.passive → 高性能滚动监听(不能与 .prevent 同用)
@keydown.ctrl.exact.s → 精确的 Ctrl+S(有其他键不触发)
@click.once → 按钮防重复点击(仅首次有效)
@touchmove.prevent → 阻止移动端页面随手势滚动
十四、在 Render Function 中绑定事件
javascript
import { h, withModifiers } from 'vue'
// withModifiers:在 render function 中应用事件修饰符
h('button', {
onClick: withModifiers(
(event: MouseEvent) => { console.log('clicked') },
['stop', 'prevent'] // 等价于 @click.stop.prevent
)
})
// 多事件监听
h('input', {
onInput: handler1,
onChange: handler2,
'onUpdate:modelValue': (val) => (modelValue.value = val)
})
// 动态事件名
const eventName = 'onClick'
h('div', { [eventName]: handler })
总结
Vue 的事件系统本质上是对原生 DOM 事件的完整代理。所有浏览器原生事件均可通过 @eventName 直接绑定,编译器负责将其转换为 onEventName 形式的 prop,运行时通过 addEventListener 实际注册。
几个关键认知:
mouseenter/mouseleave优于mouseover/mouseout(不冒泡,性能更好)focusin/focusout是父元素感知子元素焦点的正确方式- 中文输入必须处理
compositionstart/end,否则@input会在选字中间触发 @dragover.prevent是@drop能触发的前提条件Pointer Events是未来趋势,统一了鼠标、触控、笔的处理@scroll.passive是长列表滚动性能的关键修饰符