[TOC]

事件

1. 组件事件

1.1 基础事件触发

父组件 ParentComponent.vue

<template>
  <ChildComponent @submit="handleSubmit" @cancel="handleCancel" />
</template>
<script setup>
const handleSubmit = (payload) => {
  console.log('收到提交数据:', payload)
}
const handleCancel = () => {
  console.log('取消操作')
}
</script>

子组件 ChildComponent.vue

<script setup>
// 1. 声明组件可触发的事件
const emit = defineEmits(['submit', 'cancel'])
// 2. 触发无参数事件
const handleCancel = () => { emit('cancel') }
// 3. 触发带参数事件
const handleSubmit = () => {
  emit('submit', { username: 'admin', timestamp: Date.now() })
}
</script>
<template>
  <button @click="handleSubmit">提交</button>
</template>

1.2 TypeScript 类型声明

子组件(带类型约束)

<script setup lang="ts">
// 定义事件类型
interface Emits {
  (e: 'update:modelValue', value: string): void
  (e: 'search', keyword: string, page: number): void
  (e: 'error'): void
}
const emit = defineEmits<Emits>()
const handleSearch = () => {
  if (keyword.value.length > 0) {
    emit('search', keyword.value, currentPage.value)
  } else {
    emit('error')
  }
}
</script>

1.3 事件验证(高级用法)

<script setup>
const emit = defineEmits({
  // 带验证的事件
  'page-change': (page) => {
    // 返回布尔值表示验证是否通过
    return Number.isInteger(page) && page > 0
  }
})
const changePage = (newPage) => {
  if (newPage !== currentPage.value) {
    emit('page-change', newPage)
  }
}
</script>

1.4 v-model 双向绑定

1、单个 v-model

<!-- 子组件 -->
<script setup>
const emit = defineEmits(['update:modelValue'])
const updateValue = (newVal) => {
  emit('update:modelValue', newVal)
}
</script>

<!-- 父组件 -->
<ChildComponent v-model="value" />

2、多个 v-model

<!-- 子组件 -->
<script setup>
const emit = defineEmits(['update:name', 'update:age'])

const updateName = (val) => emit('update:name', val)
const updateAge = (val) => emit('update:age', val)
</script>

<!-- 父组件 -->
<ChildComponent v-model:name="userName" v-model:age="userAge" />

1.5 事件使用模式对比

模式 示例代码 适用场景
基础事件 emit('event-name') 简单状态通知
数据传递事件 emit('search', keyword) 提交表单数据
复合参数事件 emit('position', x, y) 传递多个关联参数
验证事件 带验证的 defineEmits 需要保证事件数据合法性
v-model 事件 update:modelValue 实现双向数据绑定

1.6 最佳实践

  1. 命名规范

    • 使用 kebab-case 命名事件(如 form-submit
    • 避免使用原生事件名(如 click
  2. 参数设计

    • 复杂数据使用对象包装
    emit('user-update', {
      id: 123,
      newName: 'Alice'
    })
    
  3. 类型安全

    // 使用泛型约束
    defineEmits<{
      (e: 'upload-success', file: File): void
      (e: 'upload-error', error: Error): void
    }>()
    

1.7 常见问题

  1. 为什么需要 defineEmits?

    • 明确组件接口
    • 更好的类型推断
    • 与模板自动扫描兼容
  2. 如何传递多个参数?

    emit('position-change', x, y)
    // 父组件接收:
    @position-change="(x, y) => handlePosition(x, y)"
    
  3. 如何监听原生事件?

    <!-- 子组件 -->
    <button @click="$emit('custom-click', $event)">
      按钮
    </button>
    
    <!-- 父组件 -->
    <ChildComponent @custom-click="handleClick" />
    

2. 处理原生事件

2.1 基础事件绑定

<template>
  <!-- 点击事件 -->
  <button @click="handleClick">点击我</button>
  <!-- 输入事件 -->
  <input @input="handleInput">
  <!-- 键盘事件 -->
  <input @keyup.enter="submitForm">
</template>
<script setup>
const handleClick = (event) => {}
const handleInput = (event) => {}
const submitForm = () => {}
</script>

2.2 事件修饰符

修饰符 作用 示例
.stop 阻止事件冒泡 @click.stop="handler"
.prevent 阻止默认行为 @submit.prevent="handler"
.capture 使用捕获模式 @click.capture="handler"
.self 仅当事件源是自身时触发 @click.self="handler"
.once 只触发一次 @click.once="handler"
.passive 提升滚动性能 @scroll.passive="handler"

组合使用示例

<a href="/about" @click.prevent.stop="handleLinkClick">
  阻止跳转和冒泡
</a>

2.3 按键修饰符

Vue 3 内置了以下常见按键的别名(无需记忆键值):

修饰符 对应按键
.enter Enter 回车键
.tab Tab 键
.delete Delete 或 Backspace
.esc Esc 键
.space 空格键
.up 方向键 ↑
.down 方向键 ↓
.left 方向键 ←
.right 方向键 →
<input 
  @keyup.enter="submit"
  @keyup.ctrl.s="save"
  @keydown.tab="nextField"
>

<!-- 自定义按键别名 -->
<script>
// 全局定义(main.js)
app.config.globalProperties.$customKeys = {
  vkSave: 83 // s 键
}
</script>

2.4 系统修饰键

修饰符 对应按键 典型应用场景
.ctrl Ctrl 键 组合快捷键(如 Ctrl+C)‌
.alt Alt 键 菜单快捷键或组合操作‌
.shift Shift 键 大小写切换或扩展选择‌
.meta Windows 键或 Mac 的 Command 键 系统级快捷操作‌
<div @click.ctrl.exact="ctrlClick">Ctrl + 点击</div>
<div @click.alt.shift="altShiftClick">Alt+Shift+点击</div>

<!-- exact 修饰符示例 -->
<button 
  @click.exact="onlyClick"
  @click.ctrl="ctrlClick">
  精确控制
</button>

2.5 自定义组件中的原生事件

方案1:自动继承(默认绑定到根元素)

<!-- CustomButton.vue -->
<template>
  <button class="custom-btn">
    <slot></slot>
  </button>
</template>

<!-- 父组件使用 -->
<CustomButton @click="handleButtonClick" />

方案2:手动绑定(更精细控制)

<!-- CustomInput.vue -->
<template>
  <div class="input-wrapper">
    <input v-bind="$attrs" @input="$emit('update:modelValue', $event.target.value)">
  </div>
</template>

<script>
// 禁用自动继承
defineOptions({
  inheritAttrs: false
})
</script>

<!-- 父组件使用 -->
<CustomInput 
  @input="handleInput"
  @focus="handleFocus"
  @blur="handleBlur"
/>

2.6 组合式API中的事件处理

<template>
  <div ref="elementRef">事件目标</div>
</template>
<script setup>
import { onMounted } from 'vue'
// 1. 模板引用
const elementRef = ref(null)
// 2. 手动添加事件监听
onMounted(() => {
  elementRef.value.addEventListener('custom-event', handleCustom)
})
// 3. 清理事件
onUnmounted(() => {
  elementRef.value?.removeEventListener('custom-event', handleCustom)
})
// 4. 处理函数
const handleCustom = (event) => {
  console.log('收到自定义事件:', event.detail)
}
</script>

2.7 原生事件对象的使用

<template>
  <input @input="logEvent">
</template>
<script setup>
const logEvent = (event) => {
  // 获取事件目标值
  console.log(event.target.value)
  // 获取键盘事件键位
  if (event.type === 'keyup') {
    console.log('按键代码:', event.keyCode)
  }
  // 阻止默认行为(替代 preventDefault())
  event.preventDefault()
}
</script>

2.8 高级场景:第三方库集成

<template>
  <div ref="chartContainer"></div>
</template>
<script setup>
import { onMounted, ref } from 'vue'
import * as echarts from 'echarts'
  
const chartContainer = ref(null)
let chartInstance = null
onMounted(() => {
  chartInstance = echarts.init(chartContainer.value)
  // 监听图表点击事件
  chartInstance.on('click', (params) => {
    console.log('图表点击:', params)
  })
})
// 清理事件
onUnmounted(() => {
  chartInstance?.dispose()
})
</script>

2.9 最佳实践建议

  1. 优先使用修饰符:替代手动调用 event.preventDefault()

  2. 合理使用事件委托:对列表元素使用事件委托提升性能

  3. 及时清理事件:在组件卸载时移除手动绑定的事件

  4. 类型安全(TS)

    interface CustomEventDetail {
      message: string
    }
    const handleEvent = (event: Event) => {
      const customEvent = event as CustomEvent<CustomEventDetail>
      console.log(customEvent.detail.message)
    }
    

2.10 常见问题解决

问题1:组件上的原生事件不生效

<!-- 子组件声明要触发的事件 -->
<script setup>
defineEmits(['click'])
</script>

<!-- 父组件显式监听 -->
<ChildComponent @click="handleClick" />

问题2:需要访问原生事件对象

<!-- 使用 $event 传递 -->
<button @click="handleClick($event)">按钮</button>

<script setup>
const handleClick = (event: MouseEvent) => {
  console.log(event.clientX)
}
</script>
Last Updated: 4/21/2025, 4:44:59 PM