<返回目录     Powered by claud/xia兄

第5课: 事件处理

监听事件

使用v-on指令(简写为@)监听DOM事件。

<template>
  <div>
    <!-- 完整语法 -->
    <button v-on:click="count++">点击次数: {{ count }}</button>

    <!-- 简写语法 -->
    <button @click="count++">点击次数: {{ count }}</button>

    <!-- 调用方法 -->
    <button @click="handleClick">点击我</button>
  </div>
</template>

<script setup>
import { ref } from 'vue'

const count = ref(0)

function handleClick() {
  console.log('按钮被点击了')
  count.value++
}
</script>

内联事件处理器

<template>
  <div>
    <button @click="count++">增加</button>
    <button @click="count--">减少</button>
    <button @click="say('hello')">打招呼</button>
    <p>计数: {{ count }}</p>
  </div>
</template>

<script setup>
import { ref } from 'vue'

const count = ref(0)

function say(message) {
  alert(message)
}
</script>

访问事件对象

<template>
  <div>
    <!-- 自动传入event -->
    <button @click="handleClick">点击1</button>

    <!-- 使用$event传入event -->
    <button @click="handleClickWithArg('hello', $event)">点击2</button>

    <!-- 箭头函数 -->
    <button @click="(event) => handleEvent(event)">点击3</button>
  </div>
</template>

<script setup>
function handleClick(event) {
  console.log(event.target.tagName)
}

function handleClickWithArg(message, event) {
  console.log(message, event.target)
}

function handleEvent(event) {
  console.log(event)
}
</script>

事件修饰符

Vue提供了事件修饰符来处理常见的DOM事件细节。

<template>
  <div>
    <!-- .stop 阻止事件冒泡 -->
    <div @click="outerClick" style="padding:20px;background:#f0f0f0">
      <button @click.stop="innerClick">阻止冒泡</button>
    </div>

    <!-- .prevent 阻止默认行为 -->
    <form @submit.prevent="handleSubmit">
      <input type="text" v-model="message">
      <button type="submit">提交</button>
    </form>

    <!-- .capture 使用捕获模式 -->
    <div @click.capture="captureClick" style="padding:20px;background:#e0e0e0">
      <button @click="normalClick">捕获模式</button>
    </div>

    <!-- .self 只有事件源是自身才触发 -->
    <div @click.self="selfClick" style="padding:20px;background:#d0d0d0">
      <button>自身触发</button>
    </div>

    <!-- .once 只触发一次 -->
    <button @click.once="onceClick">只触发一次</button>

    <!-- .passive 提升滚动性能 -->
    <div @scroll.passive="onScroll" style="height:100px;overflow:auto">
      <div style="height:500px">滚动区域</div>
    </div>
  </div>
</template>

<script setup>
function outerClick() {
  console.log('外部点击')
}

function innerClick() {
  console.log('内部点击')
}

function handleSubmit() {
  console.log('表单提交')
}

function captureClick() {
  console.log('捕获阶段触发')
}

function normalClick() {
  console.log('冒泡阶段触发')
}

function selfClick() {
  console.log('自身点击')
}

function onceClick() {
  console.log('只触发一次')
}

function onScroll() {
  console.log('滚动中...')
}

const message = ref('')
</script>

按键修饰符

Vue提供了按键修饰符来监听特定的键盘事件。

<template>
  <div>
    <!-- 按键别名 -->
    <input @keyup.enter="onEnter" placeholder="按回车键触发">
    <input @keyup.tab="onTab" placeholder="按Tab键触发">
    <input @keyup.delete="onDelete" placeholder="按删除键触发">
    
    <!-- 系统修饰键 -->
    <button @click.ctrl="onCtrlClick">Ctrl+点击</button>
    <button @click.shift="onShiftClick">Shift+点击</button>
    
    <!-- 精确修饰符 -->
    <button @click.ctrl.exact="onCtrlExact">仅Ctrl+点击</button>
    
    <!-- 鼠标按键修饰符 -->
    <button @click.right="onRightClick">右键点击</button>
    <button @click.middle="onMiddleClick">中键点击</button>
  </div>
</template>

<script setup>
function onEnter() {
  console.log('回车键被按下')
}

function onTab() {
  console.log('Tab键被按下')
}

function onDelete() {
  console.log('删除键被按下')
}

function onCtrlClick() {
  console.log('Ctrl+点击')
}

function onShiftClick() {
  console.log('Shift+点击')
}

function onCtrlExact() {
  console.log('仅Ctrl+点击')
}

function onRightClick() {
  console.log('右键点击')
}

function onMiddleClick() {
  console.log('中键点击')
}
</script>

方法处理器

方法处理器可以接收参数,也可以自动接收事件对象。

<template>
  <div>
    <!-- 自动传入事件对象 -->
    <button @click="handleClick">点击1</button>
    
    <!-- 手动传入参数 -->
    <button @click="handleClickWithArg('hello')">点击2</button>
    
    <!-- 同时传入参数和事件对象 -->
    <button @click="handleClickWithArgAndEvent('hello', $event)">点击3</button>
    
    <!-- 在方法中访问组件数据 -->
    <button @click="increment">增加计数: {{ count }}</button>
  </div>
</template>

<script setup>
import { ref } from 'vue'

const count = ref(0)

function handleClick(event) {
  console.log('事件对象:', event)
  console.log('目标元素:', event.target)
}

function handleClickWithArg(message) {
  console.log('消息:', message)
}

function handleClickWithArgAndEvent(message, event) {
  console.log('消息:', message)
  console.log('事件对象:', event)
}

function increment() {
  count.value++
  console.log('当前计数:', count.value)
}
</script>

事件处理原理深度解析

Vue事件系统的工作原理

Vue的事件处理系统基于原生DOM事件机制,但提供了更高级的抽象和便利性:

实际应用示例

以下是一个完整的表单验证和交互示例:

<template>
  <div class="form-container">
    <h3>用户注册表单</h3>
    
    <form @submit.prevent="handleSubmit">
      <div class="form-group">
        <label for="username">用户名:</label>
        <input 
          id="username" 
          type="text" 
          v-model="form.username" 
          @input="validateUsername"
          @blur="markUsernameTouched"
          :class="{ error: errors.username }"
        >
        <span v-if="errors.username" class="error-message">{{ errors.username }}</span>
      </div>

      <div class="form-group">
        <label for="email">邮箱:</label>
        <input 
          id="email" 
          type="email" 
          v-model="form.email" 
          @input="validateEmail"
          @keyup.enter="focusNextField"
          :class="{ error: errors.email }"
        >
        <span v-if="errors.email" class="error-message">{{ errors.email }}</span>
      </div>

      <div class="form-group">
        <label for="password">密码:</label>
        <input 
          id="password" 
          type="password" 
          v-model="form.password" 
          @input="validatePassword"
          @keyup.enter="focusNextField"
          :class="{ error: errors.password }"
        >
        <span v-if="errors.password" class="error-message">{{ errors.password }}</span>
      </div>

      <div class="form-actions">
        <button 
          type="submit" 
          :disabled="!isFormValid"
          @mouseenter="onButtonHover"
          @mouseleave="onButtonLeave"
        >
          注册
        </button>
        
        <button 
          type="button" 
          @click="resetForm"
          @keyup.escape="resetForm"
        >
          重置
        </button>
      </div>
    </form>

    <div v-if="submitted" class="success-message">
      注册成功!欢迎 {{ form.username }}
    </div>
  </div>
</template>

<script setup>
import { ref, reactive, computed } from 'vue'

const form = reactive({
  username: '',
  email: '',
  password: ''
})

const errors = reactive({
  username: '',
  email: '',
  password: ''
})

const submitted = ref(false)
const touchedFields = reactive({
  username: false,
  email: false,
  password: false
})

const isFormValid = computed(() => {
  return !errors.username && !errors.email && !errors.password &&
         form.username && form.email && form.password
})

function validateUsername() {
  if (!form.username) {
    errors.username = '用户名不能为空'
  } else if (form.username.length < 3) {
    errors.username = '用户名至少3个字符'
  } else {
    errors.username = ''
  }
}

function validateEmail() {
  const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
  if (!form.email) {
    errors.email = '邮箱不能为空'
  } else if (!emailRegex.test(form.email)) {
    errors.email = '请输入有效的邮箱地址'
  } else {
    errors.email = ''
  }
}

function validatePassword() {
  if (!form.password) {
    errors.password = '密码不能为空'
  } else if (form.password.length < 6) {
    errors.password = '密码至少6个字符'
  } else {
    errors.password = ''
  }
}

function markUsernameTouched() {
  touchedFields.username = true
  validateUsername()
}

function focusNextField(event) {
  const fields = ['username', 'email', 'password']
  const currentIndex = fields.indexOf(event.target.id)
  if (currentIndex < fields.length - 1) {
    document.getElementById(fields[currentIndex + 1])?.focus()
  }
}

function onButtonHover() {
  console.log('鼠标悬停在按钮上')
}

function onButtonLeave() {
  console.log('鼠标离开按钮')
}

function handleSubmit() {
  if (isFormValid.value) {
    console.log('提交表单:', form)
    submitted.value = true
    // 这里可以发送到服务器
  }
}

function resetForm() {
  Object.keys(form).forEach(key => {
    form[key] = ''
    errors[key] = ''
    touchedFields[key] = false
  })
  submitted.value = false
}
</script>

<style scoped>
.form-container {
  max-width: 400px;
  margin: 0 auto;
  padding: 20px;
}

.form-group {
  margin-bottom: 15px;
}

label {
  display: block;
  margin-bottom: 5px;
  font-weight: bold;
}

input {
  width: 100%;
  padding: 8px;
  border: 1px solid #ddd;
  border-radius: 4px;
}

input.error {
  border-color: #f44336;
}

.error-message {
  color: #f44336;
  font-size: 12px;
  margin-top: 5px;
}

.form-actions {
  display: flex;
  gap: 10px;
  margin-top: 20px;
}

button {
  padding: 10px 20px;
  border: none;
  border-radius: 4px;
  cursor: pointer;
}

button:disabled {
  background-color: #ccc;
  cursor: not-allowed;
}

.success-message {
  margin-top: 20px;
  padding: 10px;
  background-color: #4caf50;
  color: white;
  border-radius: 4px;
}
</style>

最佳实践与性能优化

事件处理最佳实践

常见问题与解决方案

Q: 为什么有时候事件处理函数不执行?

A: 常见原因包括:方法名拼写错误、方法未定义、事件绑定语法错误等。

Q: 如何阻止事件冒泡?

A: 使用.stop修饰符或在事件处理函数中调用event.stopPropagation()。

Q: 如何同时使用多个修饰符?

A: 可以链式使用修饰符,如@click.stop.prevent="handler"。

练习与挑战

练习1: 创建交互式计数器

创建一个计数器组件,包含以下功能:

  • 点击"+"按钮增加计数
  • 点击"-"按钮减少计数
  • 双击计数器重置为0
  • 按住Shift键点击时以5为单位增减
  • 按空格键重置计数器

练习2: 实现拖拽排序列表

创建一个可拖拽排序的待办事项列表:

  • 使用鼠标拖拽重新排序列表项
  • 点击删除按钮移除项目
  • 双击项目进入编辑模式
  • 按Enter键保存编辑,按Escape键取消

本章总结

核心知识点回顾

学习建议

<div> <!-- 阻止默认行为 --> <form @submit.prevent="onSubmit"> <button type="submit">提交</button> </form> <!-- 阻止事件冒泡 --> <div @click="divClick"> <button @click.stop="buttonClick">点击</button> </div> <!-- 捕获模式 --> <div @click.capture="handleCapture"> <button>捕获</button> </div> <!-- 只触发一次 --> <button @click.once="handleOnce">只触发一次</button> <!-- 只当事件在该元素本身触发时才触发回调 --> <div @click.self="handleSelf"> <button>点击</button> </div> <!-- 修饰符可以链式调用 --> <form @submit.prevent.stop="onSubmit"></form> </div> </template> <script setup> function onSubmit() { console.log('表单提交') } function divClick() { console.log('div被点击') } function buttonClick() { console.log('button被点击') } function handleCapture() { console.log('捕获阶段') } function handleOnce() { console.log('只执行一次') } function handleSelf() { console.log('self触发') } </script>
事件修饰符:
- .stop: 阻止事件冒泡
- .prevent: 阻止默认行为
- .capture: 使用捕获模式
- .self: 只在元素本身触发
- .once: 只触发一次
- .passive: 提升移动端性能

按键修饰符

<template>
  <div>
    <!-- 按下Enter键 -->
    <input @keyup.enter="submit" placeholder="按Enter提交">

    <!-- 按下Delete或Backspace键 -->
    <input @keyup.delete="handleDelete">

    <!-- 常用按键别名 -->
    <input @keyup.esc="handleEsc">
    <input @keyup.space="handleSpace">
    <input @keyup.tab="handleTab">
    <input @keyup.up="handleUp">
    <input @keyup.down="handleDown">

    <!-- 使用键码 -->
    <input @keyup.page-down="handlePageDown">
  </div>
</template>

<script setup>
function submit() {
  console.log('提交')
}

function handleDelete() {
  console.log('删除')
}

function handleEsc() {
  console.log('取消')
}

function handleSpace() {
  console.log('空格')
}

function handleTab() {
  console.log('Tab')
}

function handleUp() {
  console.log('向上')
}

function handleDown() {
  console.log('向下')
}

function handlePageDown() {
  console.log('PageDown')
}
</script>

系统修饰键

<template>
  <div>
    <!-- Ctrl + Click -->
    <button @click.ctrl="handleCtrlClick">Ctrl+点击</button>

    <!-- Alt + Enter -->
    <input @keyup.alt.enter="handleAltEnter">

    <!-- Shift + Click -->
    <button @click.shift="handleShiftClick">Shift+点击</button>

    <!-- Meta(Cmd/Win) + Click -->
    <button @click.meta="handleMetaClick">Meta+点击</button>

    <!-- .exact修饰符:精确匹配 -->
    <button @click.ctrl.exact="handleCtrlOnly">只有Ctrl</button>
    <button @click.exact="handleClickOnly">没有修饰键</button>
  </div>
</template>

<script setup>
function handleCtrlClick() {
  console.log('Ctrl + Click')
}

function handleAltEnter() {
  console.log('Alt + Enter')
}

function handleShiftClick() {
  console.log('Shift + Click')
}

function handleMetaClick() {
  console.log('Meta + Click')
}

function handleCtrlOnly() {
  console.log('只按了Ctrl')
}

function handleClickOnly() {
  console.log('没有修饰键')
}
</script>

鼠标按键修饰符

<template>
  <div>
    <button @click.left="handleLeftClick">左键点击</button>
    <button @click.right="handleRightClick">右键点击</button>
    <button @click.middle="handleMiddleClick">中键点击</button>
  </div>
</template>

<script setup>
function handleLeftClick() {
  console.log('左键')
}

function handleRightClick() {
  console.log('右键')
}

function handleMiddleClick() {
  console.log('中键')
}
</script>

实战示例:表单验证

<template>
  <div>
    <form @submit.prevent="handleSubmit">
      <div>
        <label>用户名:</label>
        <input
          v-model="form.username"
          @blur="validateUsername"
          @keyup.enter="handleSubmit"
        >
        <span v-if="errors.username">{{ errors.username }}</span>
      </div>

      <div>
        <label>密码:</label>
        <input
          type="password"
          v-model="form.password"
          @blur="validatePassword"
        >
        <span v-if="errors.password">{{ errors.password }}</span>
      </div>

      <button type="submit">提交</button>
      <button type="button" @click.stop="handleReset">重置</button>
    </form>
  </div>
</template>

<script setup>
import { reactive } from 'vue'

const form = reactive({
  username: '',
  password: ''
})

const errors = reactive({
  username: '',
  password: ''
})

function validateUsername() {
  if (!form.username) {
    errors.username = '用户名不能为空'
  } else if (form.username.length < 3) {
    errors.username = '用户名至少3个字符'
  } else {
    errors.username = ''
  }
}

function validatePassword() {
  if (!form.password) {
    errors.password = '密码不能为空'
  } else if (form.password.length < 6) {
    errors.password = '密码至少6个字符'
  } else {
    errors.password = ''
  }
}

function handleSubmit() {
  validateUsername()
  validatePassword()

  if (!errors.username && !errors.password) {
    console.log('提交表单', form)
    alert('提交成功!')
  }
}

function handleReset() {
  form.username = ''
  form.password = ''
  errors.username = ''
  errors.password = ''
}
</script>

练习

  1. 创建一个计数器,支持增加、减少、重置功能
  2. 实现一个搜索框,按Enter键触发搜索
  3. 创建一个右键菜单,阻止默认的浏览器右键菜单
  4. 实现一个表单,使用各种事件修饰符进行验证