Vue3 事件处理 v-on 指令 (@) 详解

Vue3 事件处理 v-on 指令 (@) 详解

核心概念理解

什么是事件处理?

事件处理就是响应用户的操作,比如点击按钮、输入文字、鼠标悬停等。Vue 通过 v-on 指令(简写为 @)来监听和处理这些事件。

基础用法

1. 简单事件处理

vue 复制代码
<template>
  <div class="event-demo">
    <h2>基础事件处理</h2>
    
    <!-- 点击事件 -->
    <button @click="handleClick">点击我</button>
    
    <!-- 双击事件 -->
    <button @dblclick="handleDoubleClick">双击我</button>
    
    <!-- 鼠标悬停事件 -->
    <div 
      @mouseenter="handleMouseEnter" 
      @mouseleave="handleMouseLeave"
      class="hover-box"
    >
      鼠标悬停区域
    </div>
    
    <!-- 输入事件 -->
    <input 
      @input="handleInput" 
      placeholder="输入文字试试"
      class="input-field"
    >
    <p>输入内容: {{ inputValue }}</p>
    
    <!-- 键盘事件 -->
    <input 
      @keyup.enter="handleEnter"
      @keyup.esc="handleEscape"
      placeholder="按 Enter 或 Esc"
      class="input-field"
    >
  </div>
</template>

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

const inputValue = ref('')

const handleClick = () => {
  alert('按钮被点击了!')
}

const handleDoubleClick = () => {
  alert('按钮被双击了!')
}

const handleMouseEnter = () => {
  console.log('鼠标进入')
}

const handleMouseLeave = () => {
  console.log('鼠标离开')
}

const handleInput = (event) => {
  inputValue.value = event.target.value
}

const handleEnter = () => {
  alert('按下了 Enter 键')
}

const handleEscape = () => {
  alert('按下了 Esc 键')
}
</script>

<style>
.event-demo {
  max-width: 600px;
  margin: 0 auto;
  padding: 20px;
}

.hover-box {
  width: 200px;
  height: 100px;
  background-color: #e3f2fd;
  border: 2px solid #2196f3;
  border-radius: 8px;
  display: flex;
  align-items: center;
  justify-content: center;
  margin: 20px 0;
  cursor: pointer;
  transition: all 0.3s ease;
}

.hover-box:hover {
  background-color: #2196f3;
  color: white;
}

.input-field {
  width: 100%;
  padding: 10px;
  margin: 10px 0;
  border: 1px solid #ced4da;
  border-radius: 4px;
  font-size: 16px;
}

button {
  padding: 10px 20px;
  margin: 5px;
  border: none;
  border-radius: 4px;
  background-color: #007bff;
  color: white;
  cursor: pointer;
  font-size: 16px;
  transition: background-color 0.2s ease;
}

button:hover {
  background-color: #0056b3;
}
</style>

事件修饰符

1. 常用事件修饰符

vue 复制代码
<template>
  <div class="modifier-demo">
    <h2>事件修饰符</h2>
    
    <!-- .prevent 阻止默认行为 -->
    <form @submit.prevent="handleSubmit">
      <input v-model="form.name" placeholder="姓名" required>
      <button type="submit">提交表单</button>
    </form>
    
    <!-- .stop 阻止事件冒泡 -->
    <div class="parent" @click="parentClick">
      父元素
      <div class="child" @click.stop="childClick">
        子元素 (阻止冒泡)
      </div>
    </div>
    
    <!-- .self 只在事件目标是元素自身时触发 -->
    <div class="self-demo" @click.self="selfClick">
      外层区域 (只点击这里才触发)
      <div class="inner">
        内层区域 (点击这里不触发)
      </div>
    </div>
    
    <!-- .once 只触发一次 -->
    <button @click.once="onceClick">
      只能点击一次的按钮
    </button>
    
    <!-- .capture 事件捕获模式 -->
    <div class="capture-demo" @click.capture="captureClick">
      捕获阶段
      <div class="capture-child" @click="childClick2">
        目标阶段
      </div>
    </div>
  </div>
</template>

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

const form = ref({
  name: ''
})

const handleSubmit = () => {
  alert(`提交成功: ${form.value.name}`)
  form.value.name = ''
}

const parentClick = () => {
  console.log('父元素被点击')
}

const childClick = () => {
  console.log('子元素被点击 (不会冒泡到父元素)')
}

const selfClick = () => {
  alert('只在点击外层区域时触发')
}

const onceClick = () => {
  alert('这个按钮只能点击一次')
}

const captureClick = () => {
  console.log('捕获阶段: 外层被点击')
}

const childClick2 = () => {
  console.log('目标阶段: 内层被点击')
}
</script>

<style>
.modifier-demo {
  max-width: 600px;
  margin: 0 auto;
  padding: 20px;
}

form {
  padding: 20px;
  background-color: #f8f9fa;
  border-radius: 8px;
  margin-bottom: 20px;
}

form input {
  width: 100%;
  padding: 10px;
  margin-bottom: 10px;
  border: 1px solid #ced4da;
  border-radius: 4px;
}

.parent {
  width: 300px;
  height: 150px;
  background-color: #d1ecf1;
  border: 2px solid #bee5eb;
  display: flex;
  align-items: center;
  justify-content: center;
  margin: 20px 0;
  position: relative;
}

.child {
  width: 150px;
  height: 75px;
  background-color: #f8d7da;
  border: 2px solid #f5c6cb;
  display: flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
}

.self-demo {
  width: 300px;
  height: 150px;
  background-color: #d4edda;
  border: 2px solid #c3e6cb;
  display: flex;
  align-items: center;
  justify-content: center;
  margin: 20px 0;
  position: relative;
}

.inner {
  width: 150px;
  height: 75px;
  background-color: #fff3cd;
  border: 2px solid #ffeaa7;
  display: flex;
  align-items: center;
  justify-content: center;
}

.capture-demo {
  width: 300px;
  height: 150px;
  background-color: #e2e3e5;
  border: 2px solid #d6d8db;
  display: flex;
  align-items: center;
  justify-content: center;
  margin: 20px 0;
}

.capture-child {
  width: 150px;
  height: 75px;
  background-color: #cce7ff;
  border: 2px solid #b8daff;
  display: flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
}
</style>

2. 按键修饰符

vue 复制代码
<template>
  <div class="key-modifier-demo">
    <h2>按键修饰符</h2>
    
    <!-- 系统按键修饰符 -->
    <div class="key-section">
      <h3>系统按键</h3>
      <input 
        @keyup.ctrl.enter="handleCtrlEnter"
        placeholder="按 Ctrl + Enter"
        class="input-field"
      >
      <input 
        @keyup.alt.c="handleAltC"
        placeholder="按 Alt + C"
        class="input-field"
      >
      <input 
        @keyup.shift.space="handleShiftSpace"
        placeholder="按 Shift + 空格"
        class="input-field"
      >
    </div>
    
    <!-- 精确按键修饰符 -->
    <div class="key-section">
      <h3>精确按键</h3>
      <input 
        @keyup.exact="handleExact"
        placeholder="只按一个键"
        class="input-field"
      >
      <input 
        @keyup.ctrl.exact="handleCtrlExact"
        placeholder="只按 Ctrl 键"
        class="input-field"
      >
    </div>
    
    <!-- 常用按键别名 -->
    <div class="key-section">
      <h3>常用按键</h3>
      <input 
        @keyup.enter="handleEnter"
        placeholder="Enter 键"
        class="input-field"
      >
      <input 
        @keyup.tab="handleTab"
        placeholder="Tab 键"
        class="input-field"
      >
      <input 
        @keyup.delete="handleDelete"
        placeholder="Delete 键"
        class="input-field"
      >
      <input 
        @keyup.esc="handleEscape"
        placeholder="Esc 键"
        class="input-field"
      >
      <input 
        @keyup.space="handleSpace"
        placeholder="空格键"
        class="input-field"
      >
      <input 
        @keyup.up="handleUp"
        placeholder="上箭头"
        class="input-field"
      >
      <input 
        @keyup.down="handleDown"
        placeholder="下箭头"
        class="input-field"
      >
    </div>
  </div>
</template>

<script setup>
const handleCtrlEnter = () => {
  alert('按下了 Ctrl + Enter')
}

const handleAltC = () => {
  alert('按下了 Alt + C')
}

const handleShiftSpace = () => {
  alert('按下了 Shift + 空格')
}

const handleExact = () => {
  alert('只按了一个键')
}

const handleCtrlExact = () => {
  alert('只按了 Ctrl 键')
}

const handleEnter = () => {
  console.log('Enter 键被按下')
}

const handleTab = () => {
  console.log('Tab 键被按下')
}

const handleDelete = () => {
  console.log('Delete 键被按下')
}

const handleEscape = () => {
  console.log('Esc 键被按下')
}

const handleSpace = () => {
  console.log('空格键被按下')
}

const handleUp = () => {
  console.log('上箭头被按下')
}

const handleDown = () => {
  console.log('下箭头被按下')
}
</script>

<style>
.key-modifier-demo {
  max-width: 600px;
  margin: 0 auto;
  padding: 20px;
}

.key-section {
  margin-bottom: 30px;
  padding: 20px;
  background-color: #f8f9fa;
  border-radius: 8px;
}

.key-section h3 {
  margin-top: 0;
  color: #495057;
}

.input-field {
  width: 100%;
  padding: 12px;
  margin: 10px 0;
  border: 1px solid #ced4da;
  border-radius: 4px;
  font-size: 16px;
  outline: none;
  transition: border-color 0.2s ease;
}

.input-field:focus {
  border-color: #007bff;
  box-shadow: 0 0 0 2px rgba(0, 123, 255, 0.25);
}
</style>

事件参数传递

1. 内联处理器中的参数

vue 复制代码
<template>
  <div class="param-demo">
    <h2>事件参数传递</h2>
    
    <!-- 传递静态参数 -->
    <div class="button-group">
      <button @click="greet('Alice')">问候 Alice</button>
      <button @click="greet('Bob')">问候 Bob</button>
      <button @click="greet('Charlie')">问候 Charlie</button>
    </div>
    
    <!-- 传递动态参数 -->
    <div class="counter-demo">
      <h3>计数器示例</h3>
      <div v-for="counter in counters" :key="counter.id">
        <span>{{ counter.name }}: {{ counter.value }}</span>
        <button @click="increment(counter, 1)">+1</button>
        <button @click="increment(counter, 5)">+5</button>
        <button @click="increment(counter, -1)">-1</button>
        <button @click="resetCounter(counter)">重置</button>
      </div>
    </div>
    
    <!-- 传递原生事件对象 -->
    <div class="event-object-demo">
      <h3>事件对象</h3>
      <input 
        @input="handleInputEvent"
        placeholder="输入文字查看事件对象"
        class="input-field"
      >
      <p>事件类型: {{ eventInfo.type }}</p>
      <p>输入值: {{ eventInfo.value }}</p>
      <p>按键: {{ eventInfo.key }}</p>
    </div>
    
    <!-- 方法处理器 -->
    <div class="method-handler-demo">
      <h3>方法处理器</h3>
      <button @click="handleButtonClick">点击获取事件对象</button>
      <p>按钮坐标: {{ buttonPosition.x }}, {{ buttonPosition.y }}</p>
    </div>
  </div>
</template>

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

const counters = ref([
  { id: 1, name: '计数器1', value: 0 },
  { id: 2, name: '计数器2', value: 0 },
  { id: 3, name: '计数器3', value: 0 }
])

const eventInfo = ref({
  type: '',
  value: '',
  key: ''
})

const buttonPosition = ref({
  x: 0,
  y: 0
})

const greet = (name) => {
  alert(`你好, ${name}!`)
}

const increment = (counter, amount) => {
  counter.value += amount
}

const resetCounter = (counter) => {
  counter.value = 0
}

const handleInputEvent = (event) => {
  eventInfo.value = {
    type: event.type,
    value: event.target.value,
    key: event.key
  }
}

const handleButtonClick = (event) => {
  buttonPosition.value = {
    x: event.clientX,
    y: event.clientY
  }
  console.log('事件对象:', event)
}
</script>

<style>
.param-demo {
  max-width: 800px;
  margin: 0 auto;
  padding: 20px;
}

.button-group {
  margin-bottom: 30px;
  padding: 20px;
  background-color: #f8f9fa;
  border-radius: 8px;
  text-align: center;
}

.button-group button {
  padding: 10px 15px;
  margin: 5px;
  border: none;
  border-radius: 4px;
  background-color: #28a745;
  color: white;
  cursor: pointer;
  transition: background-color 0.2s ease;
}

.button-group button:hover {
  background-color: #218838;
}

.counter-demo {
  margin-bottom: 30px;
  padding: 20px;
  background-color: #fff;
  border: 1px solid #dee2e6;
  border-radius: 8px;
}

.counter-demo div {
  display: flex;
  align-items: center;
  gap: 10px;
  margin: 10px 0;
  padding: 10px;
  background-color: #f8f9fa;
  border-radius: 4px;
}

.counter-demo button {
  padding: 5px 10px;
  border: none;
  border-radius: 3px;
  background-color: #007bff;
  color: white;
  cursor: pointer;
  font-size: 12px;
}

.counter-demo button:hover {
  background-color: #0056b3;
}

.event-object-demo {
  margin-bottom: 30px;
  padding: 20px;
  background-color: #e3f2fd;
  border-radius: 8px;
}

.method-handler-demo {
  padding: 20px;
  background-color: #fff3cd;
  border-radius: 8px;
}

.input-field {
  width: 100%;
  padding: 10px;
  margin: 10px 0;
  border: 1px solid #ced4da;
  border-radius: 4px;
  font-size: 16px;
}
</style>

实际应用示例

1. 表单处理

vue 复制代码
<template>
  <div class="form-demo">
    <h2>表单事件处理</h2>
    
    <form @submit.prevent="handleSubmit" class="user-form">
      <div class="form-group">
        <label>用户名:</label>
        <input 
          v-model="form.username"
          @blur="validateUsername"
          :class="{ 'error': errors.username }"
          required
        >
        <span v-if="errors.username" class="error-message">
          {{ errors.username }}
        </span>
      </div>
      
      <div class="form-group">
        <label>邮箱:</label>
        <input 
          v-model="form.email"
          @input="validateEmail"
          :class="{ 'error': errors.email }"
          type="email"
          required
        >
        <span v-if="errors.email" class="error-message">
          {{ errors.email }}
        </span>
      </div>
      
      <div class="form-group">
        <label>密码:</label>
        <input 
          v-model="form.password"
          @keyup.enter="handleSubmit"
          :class="{ 'error': errors.password }"
          type="password"
          required
        >
        <span v-if="errors.password" class="error-message">
          {{ errors.password }}
        </span>
      </div>
      
      <div class="form-group">
        <label>
          <input 
            v-model="form.agree"
            type="checkbox"
          >
          我同意用户协议
        </label>
      </div>
      
      <div class="form-actions">
        <button 
          type="submit" 
          :disabled="!isFormValid"
          @click.ctrl.exact="submitWithCtrl"
        >
          提交 (Ctrl+点击快速提交)
        </button>
        <button @click="resetForm" type="button">重置</button>
      </div>
    </form>
    
    <div v-if="submitted" class="success-message">
      <h3>提交成功!</h3>
      <p>用户名: {{ submittedData.username }}</p>
      <p>邮箱: {{ submittedData.email }}</p>
    </div>
  </div>
</template>

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

const form = ref({
  username: '',
  email: '',
  password: '',
  agree: false
})

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

const submitted = ref(false)
const submittedData = ref({})

// 表单验证
const validateUsername = () => {
  if (form.value.username.length < 3) {
    errors.value.username = '用户名至少3个字符'
  } else {
    errors.value.username = ''
  }
}

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

const validatePassword = () => {
  if (form.value.password.length < 6) {
    errors.value.password = '密码至少6个字符'
  } else {
    errors.value.password = ''
  }
}

// 计算属性:表单是否有效
const isFormValid = computed(() => {
  return (
    form.value.username.length >= 3 &&
    /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(form.value.email) &&
    form.value.password.length >= 6 &&
    form.value.agree
  )
})

// 表单提交
const handleSubmit = () => {
  validateUsername()
  validateEmail()
  validatePassword()
  
  if (isFormValid.value) {
    submitted.value = true
    submittedData.value = { ...form.value }
    console.log('表单提交:', form.value)
  } else {
    alert('请检查表单信息')
  }
}

// Ctrl+点击提交
const submitWithCtrl = (event) => {
  event.preventDefault()
  console.log('Ctrl+点击提交')
  handleSubmit()
}

// 重置表单
const resetForm = () => {
  form.value = {
    username: '',
    email: '',
    password: '',
    agree: false
  }
  errors.value = {
    username: '',
    email: '',
    password: ''
  }
  submitted.value = false
}
</script>

<style>
.form-demo {
  max-width: 500px;
  margin: 0 auto;
  padding: 20px;
}

.user-form {
  background-color: #fff;
  padding: 30px;
  border-radius: 8px;
  box-shadow: 0 2px 10px rgba(0,0,0,0.1);
}

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

.form-group label {
  display: block;
  margin-bottom: 5px;
  font-weight: bold;
  color: #495057;
}

.form-group input[type="text"],
.form-group input[type="email"],
.form-group input[type="password"] {
  width: 100%;
  padding: 10px;
  border: 1px solid #ced4da;
  border-radius: 4px;
  font-size: 16px;
  transition: border-color 0.2s ease;
}

.form-group input.error {
  border-color: #dc3545;
}

.form-group input:focus {
  outline: none;
  border-color: #007bff;
  box-shadow: 0 0 0 2px rgba(0, 123, 255, 0.25);
}

.error-message {
  color: #dc3545;
  font-size: 14px;
  margin-top: 5px;
  display: block;
}

.form-group input[type="checkbox"] {
  margin-right: 8px;
}

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

.form-actions button {
  padding: 12px 24px;
  border: none;
  border-radius: 4px;
  font-size: 16px;
  cursor: pointer;
  transition: all 0.2s ease;
}

.form-actions button[type="submit"] {
  background-color: #28a745;
  color: white;
  flex: 1;
}

.form-actions button[type="submit"]:hover:not(:disabled) {
  background-color: #218838;
}

.form-actions button[type="submit"]:disabled {
  background-color: #6c757d;
  cursor: not-allowed;
}

.form-actions button[type="button"] {
  background-color: #6c757d;
  color: white;
}

.form-actions button[type="button"]:hover {
  background-color: #5a6268;
}

.success-message {
  margin-top: 20px;
  padding: 20px;
  background-color: #d4edda;
  border: 1px solid #c3e6cb;
  border-radius: 8px;
  text-align: center;
}
</style>

2. 鼠标和触摸事件

vue 复制代码
<template>
  <div class="mouse-demo">
    <h2>鼠标和触摸事件</h2>
    
    <!-- 鼠标事件 -->
    <div class="mouse-section">
      <h3>鼠标事件</h3>
      <div 
        class="mouse-area"
        @mousedown="handleMouseDown"
        @mouseup="handleMouseUp"
        @mouseenter="handleMouseEnter"
        @mouseleave="handleMouseLeave"
        @mousemove="handleMouseMove"
        @contextmenu.prevent="handleRightClick"
      >
        鼠标操作区域
        <div class="mouse-info">
          <p>鼠标位置: {{ mousePosition.x }}, {{ mousePosition.y }}</p>
          <p>鼠标状态: {{ mouseStatus }}</p>
        </div>
      </div>
    </div>
    
    <!-- 拖拽功能 -->
    <div class="drag-section">
      <h3>拖拽功能</h3>
      <div 
        class="draggable-box"
        :style="{
          left: boxPosition.x + 'px',
          top: boxPosition.y + 'px',
          cursor: isDragging ? 'grabbing' : 'grab'
        }"
        @mousedown="startDrag"
        @touchstart="startDrag"
      >
        拖拽我
      </div>
    </div>
    
    <!-- 滚轮事件 -->
    <div class="wheel-section">
      <h3>滚轮事件</h3>
      <div 
        class="wheel-area"
        @wheel.prevent="handleWheel"
        :style="{ fontSize: fontSize + 'px' }"
      >
        使用鼠标滚轮调整字体大小
        <p>当前字体大小: {{ fontSize }}px</p>
      </div>
    </div>
  </div>
</template>

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

const mousePosition = ref({ x: 0, y: 0 })
const mouseStatus = ref('未按下')
const isDragging = ref(false)
const dragStart = ref({ x: 0, y: 0 })
const boxPosition = ref({ x: 50, y: 50 })
const fontSize = ref(16)

const handleMouseDown = (event) => {
  mouseStatus.value = '按下'
  updateMousePosition(event)
}

const handleMouseUp = (event) => {
  mouseStatus.value = '释放'
  updateMousePosition(event)
}

const handleMouseEnter = (event) => {
  mouseStatus.value = '进入'
  updateMousePosition(event)
}

const handleMouseLeave = (event) => {
  mouseStatus.value = '离开'
  updateMousePosition(event)
}

const handleMouseMove = (event) => {
  updateMousePosition(event)
}

const handleRightClick = () => {
  alert('右键菜单被阻止了')
}

const updateMousePosition = (event) => {
  mousePosition.value = {
    x: event.clientX,
    y: event.clientY
  }
}

// 拖拽功能
const startDrag = (event) => {
  isDragging.value = true
  const clientX = event.clientX || event.touches[0].clientX
  const clientY = event.clientY || event.touches[0].clientY
  
  dragStart.value = {
    x: clientX - boxPosition.value.x,
    y: clientY - boxPosition.value.y
  }
  
  document.addEventListener('mousemove', drag)
  document.addEventListener('touchmove', drag)
  document.addEventListener('mouseup', stopDrag)
  document.addEventListener('touchend', stopDrag)
}

const drag = (event) => {
  if (!isDragging.value) return
  
  const clientX = event.clientX || event.touches[0].clientX
  const clientY = event.clientY || event.touches[0].clientY
  
  boxPosition.value = {
    x: clientX - dragStart.value.x,
    y: clientY - dragStart.value.y
  }
}

const stopDrag = () => {
  isDragging.value = false
  document.removeEventListener('mousemove', drag)
  document.removeEventListener('touchmove', drag)
  document.removeEventListener('mouseup', stopDrag)
  document.removeEventListener('touchend', stopDrag)
}

// 滚轮事件
const handleWheel = (event) => {
  if (event.deltaY < 0) {
    fontSize.value = Math.min(fontSize.value + 2, 40)
  } else {
    fontSize.value = Math.max(fontSize.value - 2, 12)
  }
}
</script>

<style>
.mouse-demo {
  max-width: 800px;
  margin: 0 auto;
  padding: 20px;
}

.mouse-section, .drag-section, .wheel-section {
  margin-bottom: 30px;
  padding: 20px;
  background-color: #fff;
  border: 1px solid #dee2e6;
  border-radius: 8px;
}

.mouse-area {
  width: 100%;
  height: 200px;
  background-color: #e3f2fd;
  border: 2px dashed #2196f3;
  border-radius: 8px;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  position: relative;
  user-select: none;
}

.mouse-info {
  margin-top: 20px;
  text-align: center;
  background-color: rgba(255, 255, 255, 0.8);
  padding: 10px;
  border-radius: 4px;
}

.drag-section {
  position: relative;
  height: 300px;
  overflow: hidden;
}

.draggable-box {
  position: absolute;
  width: 100px;
  height: 100px;
  background-color: #ff6b6b;
  border-radius: 8px;
  display: flex;
  align-items: center;
  justify-content: center;
  color: white;
  font-weight: bold;
  user-select: none;
  transition: none;
}

.wheel-area {
  width: 100%;
  height: 200px;
  background-color: #d4edda;
  border: 2px solid #c3e6cb;
  border-radius: 8px;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  text-align: center;
  user-select: none;
}
</style>

注意事项和最佳实践

1. 事件处理中的 this 绑定

vue 复制代码
<template>
  <div class="this-demo">
    <h2>this 绑定问题</h2>
    
    <!-- ❌ 错误示例:箭头函数可能导致 this 问题 -->
    <button @click="wrongHandler">错误处理</button>
    
    <!-- ✅ 正确示例:使用普通函数 -->
    <button @click="correctHandler">正确处理</button>
    
    <!-- ✅ 正确示例:使用箭头函数但不依赖 this -->
    <button @click="() => console.log('箭头函数')">箭头函数</button>
  </div>
</template>

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

const message = ref('Hello Vue3!')

// ❌ 可能有问题的处理方式
const wrongHandler = () => {
  // 在 setup 中,this 是 undefined
  console.log(this) // undefined
}

// ✅ 推荐的处理方式
const correctHandler = () => {
  console.log(message.value) // 直接使用响应式变量
}
</script>

2. 事件性能优化

vue 复制代码
<template>
  <div class="performance-demo">
    <h2>事件性能优化</h2>
    
    <!-- ✅ 使用 .once 修饰符 -->
    <button @click.once="handleOnce">只执行一次</button>
    
    <!-- ✅ 使用 .passive 修饰符提高滚动性能 -->
    <div 
      class="scroll-area"
      @scroll.passive="handleScroll"
    >
      <div class="scroll-content">
        <div 
          v-for="item in 100" 
          :key="item"
          class="scroll-item"
        >
          项目 {{ item }}
        </div>
      </div>
    </div>
    
    <!-- ✅ 防抖处理 -->
    <div class="debounce-demo">
      <input 
        @input="debouncedInput"
        placeholder="输入文字(防抖)"
        class="input-field"
      >
      <p>防抖结果: {{ debounceResult }}</p>
    </div>
    
    <!-- ✅ 节流处理 -->
    <div class="throttle-demo">
      <div 
        @mousemove="throttledMouseMove"
        class="throttle-area"
      >
        鼠标移动区域(节流)
        <p>节流结果: {{ throttleResult }}</p>
      </div>
    </div>
  </div>
</template>

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

const debounceResult = ref('')
const throttleResult = ref('')
let debounceTimer = null
let throttleTimer = null
let lastThrottleTime = 0
const throttleDelay = 100

const handleOnce = () => {
  alert('这个按钮只能点击一次')
}

const handleScroll = (event) => {
  console.log('滚动事件触发')
}

// 防抖处理
const debouncedInput = (event) => {
  clearTimeout(debounceTimer)
  debounceTimer = setTimeout(() => {
    debounceResult.value = event.target.value
  }, 300)
}

// 节流处理
const throttledMouseMove = (event) => {
  const now = Date.now()
  if (now - lastThrottleTime > throttleDelay) {
    throttleResult.value = `X: ${event.clientX}, Y: ${event.clientY}`
    lastThrottleTime = now
  }
}
</script>

<style>
.performance-demo {
  max-width: 600px;
  margin: 0 auto;
  padding: 20px;
}

.scroll-area {
  height: 200px;
  overflow-y: auto;
  border: 1px solid #ced4da;
  border-radius: 4px;
  margin: 20px 0;
}

.scroll-content {
  padding: 10px;
}

.scroll-item {
  padding: 10px;
  margin: 5px 0;
  background-color: #f8f9fa;
  border-radius: 4px;
}

.debounce-demo, .throttle-demo {
  margin: 20px 0;
  padding: 20px;
  background-color: #f8f9fa;
  border-radius: 8px;
}

.throttle-area {
  height: 150px;
  background-color: #e3f2fd;
  border: 2px dashed #2196f3;
  border-radius: 8px;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  margin-top: 10px;
}

.input-field {
  width: 100%;
  padding: 10px;
  border: 1px solid #ced4da;
  border-radius: 4px;
  font-size: 16px;
}

button {
  padding: 10px 20px;
  margin: 5px;
  border: none;
  border-radius: 4px;
  background-color: #007bff;
  color: white;
  cursor: pointer;
  font-size: 16px;
}
</style>

总结

事件修饰符速查表

修饰符 作用
.prevent 阻止默认行为
.stop 阻止事件冒泡
.self 只在事件目标是元素自身时触发
.once 只触发一次
.capture 使用事件捕获模式
.passive 以 passive 模式添加事件

按键修饰符速查表

修饰符 对应按键
.enter Enter
.tab Tab
.delete Delete 或 Backspace
.esc Escape
.space 空格
.up 上箭头
.down 下箭头
.left 左箭头
.right 右箭头
.ctrl Ctrl
.alt Alt
.shift Shift
.meta Meta (Mac: ⌘, Windows: ⊞)

使用建议

  1. 优先使用简写@click 而不是 v-on:click
  2. 合理使用修饰符:避免在 JavaScript 中处理本可以用修饰符解决的问题
  3. 注意性能:对于频繁触发的事件使用防抖或节流
  4. 事件委托:对于大量相似元素,考虑在父元素上处理事件
  5. 清理事件:记得在组件销毁时清理手动添加的事件监听器

记忆口诀

  • @click:点击事件最常用
  • @input:输入实时响应
  • @submit.prevent:表单提交防跳转
  • @keyup.enter:回车键触发
  • 修饰符连用:功能组合更强大
相关推荐
恋猫de小郭11 小时前
Flutter Zero 是什么?它的出现有什么意义?为什么你需要了解下?
android·前端·flutter
崔庆才丨静觅18 小时前
hCaptcha 验证码图像识别 API 对接教程
前端
passerby606118 小时前
完成前端时间处理的另一块版图
前端·github·web components
掘了19 小时前
「2025 年终总结」在所有失去的人中,我最怀念我自己
前端·后端·年终总结
崔庆才丨静觅19 小时前
实用免费的 Short URL 短链接 API 对接说明
前端
崔庆才丨静觅19 小时前
5分钟快速搭建 AI 平台并用它赚钱!
前端
崔庆才丨静觅19 小时前
比官方便宜一半以上!Midjourney API 申请及使用
前端
Moment19 小时前
富文本编辑器在 AI 时代为什么这么受欢迎
前端·javascript·后端
崔庆才丨静觅20 小时前
刷屏全网的“nano-banana”API接入指南!0.1元/张量产高清创意图,开发者必藏
前端
剪刀石头布啊20 小时前
jwt介绍
前端