Skip to content

事件处理

Vue 使用 v-on 指令(简写为 @)来监听 DOM 事件,并在事件触发时执行相应的 JavaScript 代码。

基本用法

内联事件处理器

可以直接在 v-on 指令中编写简单的 JavaScript 表达式:

vue
<template>
  <div>
    <p>{{ count }}</p>
    <button @click="count++">增加 1</button>
    <button @click="count--">减少 1</button>
    <button @click="count = 0">重置</button>
  </div>
</template>

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

const count = ref(0)
</script>

方法事件处理器

对于复杂的逻辑,应该使用方法事件处理器:

vue
<template>
  <div>
    <p>{{ message }}</p>
    <button @click="greet">问候</button>
    <button @click="say('hello')">说 Hello</button>
    <button @click="say('goodbye')">说 Goodbye</button>
  </div>
</template>

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

const message = ref('Hello Vue!')

const greet = () => {
  alert('Hello!')
}

const say = (msg) => {
  message.value = msg
}
</script>

事件对象

访问原生事件

在内联事件处理器中,可以使用特殊的 $event 变量访问原生 DOM 事件:

vue
<template>
  <div>
    <button @click="warn('Form cannot be submitted yet.', $event)">
      提交
    </button>
    
    <input @keyup="handleKeyup($event)" placeholder="按任意键..." />
  </div>
</template>

<script setup>
const warn = (message, event) => {
  // 现在可以访问原生事件
  if (event) {
    event.preventDefault()
  }
  alert(message)
}

const handleKeyup = (event) => {
  console.log('按下的键:', event.key)
  console.log('键码:', event.keyCode)
}
</script>

方法中的事件对象

在方法事件处理器中,事件对象会自动作为第一个参数传入:

vue
<template>
  <div>
    <form @submit="handleSubmit">
      <input v-model="name" placeholder="姓名" required />
      <button type="submit">提交</button>
    </form>
    
    <div @click="handleClick">
      <button @click="handleButtonClick">按钮</button>
    </div>
  </div>
</template>

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

const name = ref('')

const handleSubmit = (event) => {
  event.preventDefault()
  console.log('提交的姓名:', name.value)
}

const handleClick = (event) => {
  console.log('点击了 div')
}

const handleButtonClick = (event) => {
  event.stopPropagation() // 阻止事件冒泡
  console.log('点击了按钮')
}
</script>

事件修饰符

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

基本修饰符

vue
<template>
  <div>
    <!-- 阻止单击事件继续传播 -->
    <a @click.stop="doThis">链接</a>

    <!-- 提交事件不再重载页面 -->
    <form @submit.prevent="onSubmit">
      <button type="submit">提交</button>
    </form>

    <!-- 修饰符可以串联 -->
    <a @click.stop.prevent="doThat">链接</a>

    <!-- 只有修饰符 -->
    <form @submit.prevent></form>

    <!-- 添加事件监听器时使用事件捕获模式 -->
    <div @click.capture="doThis">...</div>

    <!-- 只当在 event.target 是当前元素自身时触发处理函数 -->
    <div @click.self="doThat">...</div>

    <!-- 点击事件将只会触发一次 -->
    <a @click.once="doThis">链接</a>

    <!-- 滚动事件的默认行为 (即滚动行为) 将会立即触发 -->
    <!-- 而不会等待 `onScroll` 完成 -->
    <div @scroll.passive="onScroll">...</div>
  </div>
</template>

<script setup>
const doThis = () => {
  console.log('doThis')
}

const doThat = () => {
  console.log('doThat')
}

const onSubmit = () => {
  console.log('表单提交')
}

const onScroll = () => {
  console.log('滚动事件')
}
</script>

修饰符详解

.stop

阻止事件冒泡:

vue
<template>
  <div @click="parentClick" class="parent">
    父元素
    <button @click.stop="childClick" class="child">
      子元素(不会触发父元素事件)
    </button>
  </div>
</template>

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

const childClick = () => {
  console.log('子元素被点击')
}
</script>

.prevent

阻止默认行为:

vue
<template>
  <div>
    <!-- 阻止表单默认提交行为 -->
    <form @submit.prevent="handleSubmit">
      <input type="text" v-model="input" />
      <button type="submit">提交</button>
    </form>
    
    <!-- 阻止链接默认跳转行为 -->
    <a href="https://vuejs.org" @click.prevent="handleLinkClick">
      Vue.js 官网
    </a>
  </div>
</template>

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

const input = ref('')

const handleSubmit = () => {
  console.log('自定义提交逻辑')
}

const handleLinkClick = () => {
  console.log('自定义链接点击逻辑')
}
</script>

.self

只在事件目标是元素自身时触发:

vue
<template>
  <div @click.self="handleSelfClick" class="container">
    点击这个区域(不包括按钮)
    <button @click="handleButtonClick">按钮</button>
  </div>
</template>

<script setup>
const handleSelfClick = () => {
  console.log('只有点击 div 自身才会触发')
}

const handleButtonClick = () => {
  console.log('按钮被点击')
}
</script>

.once

事件只触发一次:

vue
<template>
  <div>
    <button @click.once="handleOnce">只能点击一次</button>
    <button @click="handleMultiple">可以多次点击</button>
  </div>
</template>

<script setup>
const handleOnce = () => {
  console.log('这个事件只会触发一次')
}

const handleMultiple = () => {
  console.log('这个事件可以多次触发')
}
</script>

按键修饰符

基本按键修饰符

vue
<template>
  <div>
    <!-- 只有在 `key` 是 `Enter` 时调用 `submit` -->
    <input @keyup.enter="submit" placeholder="按 Enter 提交" />

    <!-- 只有在 `key` 是 `Escape` 时调用 `clear` -->
    <input @keyup.esc="clear" placeholder="按 Esc 清空" />

    <!-- 只有在 `key` 是 `Space` 时调用 `handleSpace` -->
    <input @keyup.space="handleSpace" placeholder="按空格键" />

    <!-- 只有在 `key` 是 `Tab` 时调用 `handleTab` -->
    <input @keydown.tab="handleTab" placeholder="按 Tab 键" />

    <!-- 只有在 `key` 是 `Delete` 或 `Backspace` 时调用 `handleDelete` -->
    <input @keyup.delete="handleDelete" placeholder="按删除键" />
  </div>
</template>

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

const clear = () => {
  console.log('清空')
}

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

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

const handleDelete = () => {
  console.log('删除键被按下')
}
</script>

系统修饰符

vue
<template>
  <div>
    <!-- Alt + Enter -->
    <input @keyup.alt.enter="clear" placeholder="Alt + Enter" />

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

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

    <!-- Meta + Click (Mac 上的 Cmd 键,Windows 上的 Windows 键) -->
    <div @click.meta="doSomething">Meta + 点击</div>
  </div>
</template>

<script setup>
const clear = () => {
  console.log('Alt + Enter')
}

const doSomething = () => {
  console.log('系统键 + 点击')
}
</script>

.exact 修饰符

.exact 修饰符允许你控制由精确的系统修饰符组合触发的事件:

vue
<template>
  <div>
    <!-- 即使 Alt 或 Shift 被一同按下时也会触发 -->
    <button @click.ctrl="onClick">A</button>

    <!-- 有且只有 Ctrl 被按下的时候才触发 -->
    <button @click.ctrl.exact="onCtrlClick">B</button>

    <!-- 没有任何系统修饰符被按下的时候才触发 -->
    <button @click.exact="onClick">C</button>
  </div>
</template>

<script setup>
const onClick = () => {
  console.log('普通点击')
}

const onCtrlClick = () => {
  console.log('只有 Ctrl + 点击')
}
</script>

鼠标按钮修饰符

vue
<template>
  <div>
    <!-- 左键点击 -->
    <button @click.left="handleLeftClick">左键点击</button>

    <!-- 右键点击 -->
    <button @click.right="handleRightClick">右键点击</button>

    <!-- 中键点击 -->
    <button @click.middle="handleMiddleClick">中键点击</button>
  </div>
</template>

<script setup>
const handleLeftClick = () => {
  console.log('左键点击')
}

const handleRightClick = () => {
  console.log('右键点击')
}

const handleMiddleClick = () => {
  console.log('中键点击')
}
</script>

常见事件类型

表单事件

vue
<template>
  <form @submit.prevent="handleSubmit">
    <!-- 输入事件 -->
    <input
      v-model="username"
      @input="handleInput"
      @focus="handleFocus"
      @blur="handleBlur"
      @change="handleChange"
      placeholder="用户名"
    />

    <!-- 选择事件 -->
    <select @change="handleSelectChange" v-model="selectedOption">
      <option value="">请选择</option>
      <option value="option1">选项 1</option>
      <option value="option2">选项 2</option>
    </select>

    <!-- 复选框事件 -->
    <label>
      <input
        type="checkbox"
        v-model="agreed"
        @change="handleCheckboxChange"
      />
      我同意条款
    </label>

    <button type="submit">提交</button>
  </form>
</template>

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

const username = ref('')
const selectedOption = ref('')
const agreed = ref(false)

const handleSubmit = () => {
  console.log('提交表单:', {
    username: username.value,
    selectedOption: selectedOption.value,
    agreed: agreed.value
  })
}

const handleInput = (event) => {
  console.log('输入中:', event.target.value)
}

const handleFocus = () => {
  console.log('获得焦点')
}

const handleBlur = () => {
  console.log('失去焦点')
}

const handleChange = () => {
  console.log('值改变:', username.value)
}

const handleSelectChange = () => {
  console.log('选择改变:', selectedOption.value)
}

const handleCheckboxChange = () => {
  console.log('复选框状态:', agreed.value)
}
</script>

鼠标事件

vue
<template>
  <div
    class="mouse-area"
    @click="handleClick"
    @dblclick="handleDoubleClick"
    @mousedown="handleMouseDown"
    @mouseup="handleMouseUp"
    @mouseover="handleMouseOver"
    @mouseout="handleMouseOut"
    @mousemove="handleMouseMove"
    @contextmenu.prevent="handleContextMenu"
  >
    <p>鼠标事件测试区域</p>
    <p>坐标: ({{ mouseX }}, {{ mouseY }})</p>
  </div>
</template>

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

const mouseX = ref(0)
const mouseY = ref(0)

const handleClick = (event) => {
  console.log('单击', event.clientX, event.clientY)
}

const handleDoubleClick = () => {
  console.log('双击')
}

const handleMouseDown = () => {
  console.log('鼠标按下')
}

const handleMouseUp = () => {
  console.log('鼠标释放')
}

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

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

const handleMouseMove = (event) => {
  mouseX.value = event.clientX
  mouseY.value = event.clientY
}

const handleContextMenu = () => {
  console.log('右键菜单')
}
</script>

<style scoped>
.mouse-area {
  width: 300px;
  height: 200px;
  border: 2px solid #ccc;
  padding: 20px;
  margin: 20px 0;
  background-color: #f9f9f9;
  cursor: pointer;
}
</style>

键盘事件

vue
<template>
  <div>
    <input
      v-model="inputValue"
      @keydown="handleKeyDown"
      @keyup="handleKeyUp"
      @keypress="handleKeyPress"
      placeholder="键盘事件测试"
    />
    
    <p>最后按下的键: {{ lastKey }}</p>
    <p>按键次数: {{ keyCount }}</p>
    
    <!-- 特定按键处理 -->
    <input
      @keyup.enter="handleEnter"
      @keyup.esc="handleEscape"
      @keyup.arrow-up="handleArrowUp"
      @keyup.arrow-down="handleArrowDown"
      placeholder="特定按键测试"
    />
  </div>
</template>

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

const inputValue = ref('')
const lastKey = ref('')
const keyCount = ref(0)

const handleKeyDown = (event) => {
  console.log('按键按下:', event.key)
}

const handleKeyUp = (event) => {
  lastKey.value = event.key
  keyCount.value++
  console.log('按键释放:', event.key)
}

const handleKeyPress = (event) => {
  console.log('按键按压:', event.key)
}

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

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

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

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

实际应用示例

搜索框

vue
<template>
  <div class="search-container">
    <div class="search-box">
      <input
        v-model="searchQuery"
        @input="handleInput"
        @keyup.enter="handleSearch"
        @keyup.esc="clearSearch"
        @focus="showSuggestions = true"
        @blur="hideSuggestions"
        placeholder="搜索..."
        class="search-input"
      />
      <button @click="handleSearch" class="search-button">
        搜索
      </button>
    </div>
    
    <div v-if="showSuggestions && suggestions.length" class="suggestions">
      <div
        v-for="(suggestion, index) in suggestions"
        :key="index"
        @mousedown.prevent="selectSuggestion(suggestion)"
        class="suggestion-item"
      >
        {{ suggestion }}
      </div>
    </div>
    
    <div v-if="searchResults.length" class="results">
      <h3>搜索结果:</h3>
      <ul>
        <li v-for="result in searchResults" :key="result.id">
          {{ result.title }}
        </li>
      </ul>
    </div>
  </div>
</template>

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

const searchQuery = ref('')
const showSuggestions = ref(false)
const searchResults = ref([])

const allSuggestions = [
  'Vue.js',
  'React',
  'Angular',
  'JavaScript',
  'TypeScript',
  'Node.js',
  'Express',
  'MongoDB'
]

const suggestions = computed(() => {
  if (!searchQuery.value) return []
  return allSuggestions.filter(item =>
    item.toLowerCase().includes(searchQuery.value.toLowerCase())
  ).slice(0, 5)
})

const handleInput = () => {
  // 实时搜索建议
  showSuggestions.value = true
}

const handleSearch = () => {
  if (searchQuery.value.trim()) {
    // 模拟搜索结果
    searchResults.value = [
      { id: 1, title: `关于 "${searchQuery.value}" 的结果 1` },
      { id: 2, title: `关于 "${searchQuery.value}" 的结果 2` },
      { id: 3, title: `关于 "${searchQuery.value}" 的结果 3` }
    ]
    showSuggestions.value = false
  }
}

const clearSearch = () => {
  searchQuery.value = ''
  searchResults.value = []
  showSuggestions.value = false
}

const selectSuggestion = (suggestion) => {
  searchQuery.value = suggestion
  handleSearch()
}

const hideSuggestions = () => {
  // 延迟隐藏,以便点击建议项
  setTimeout(() => {
    showSuggestions.value = false
  }, 200)
}
</script>

<style scoped>
.search-container {
  position: relative;
  max-width: 500px;
  margin: 20px auto;
}

.search-box {
  display: flex;
  border: 2px solid #ddd;
  border-radius: 25px;
  overflow: hidden;
}

.search-input {
  flex: 1;
  padding: 12px 20px;
  border: none;
  outline: none;
  font-size: 16px;
}

.search-button {
  padding: 12px 20px;
  background-color: #007bff;
  color: white;
  border: none;
  cursor: pointer;
  font-size: 16px;
}

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

.suggestions {
  position: absolute;
  top: 100%;
  left: 0;
  right: 0;
  background: white;
  border: 1px solid #ddd;
  border-top: none;
  border-radius: 0 0 8px 8px;
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
  z-index: 1000;
}

.suggestion-item {
  padding: 12px 20px;
  cursor: pointer;
  border-bottom: 1px solid #eee;
}

.suggestion-item:hover {
  background-color: #f5f5f5;
}

.suggestion-item:last-child {
  border-bottom: none;
}

.results {
  margin-top: 20px;
  padding: 20px;
  background-color: #f9f9f9;
  border-radius: 8px;
}

.results ul {
  list-style: none;
  padding: 0;
}

.results li {
  padding: 8px 0;
  border-bottom: 1px solid #ddd;
}

.results li:last-child {
  border-bottom: none;
}
</style>

拖拽功能

vue
<template>
  <div class="drag-container">
    <h3>拖拽示例</h3>
    
    <div
      class="draggable-item"
      :style="{ transform: `translate(${position.x}px, ${position.y}px)` }"
      @mousedown="startDrag"
    >
      拖拽我
    </div>
    
    <div class="info">
      <p>位置: ({{ position.x }}, {{ position.y }})</p>
      <p>状态: {{ isDragging ? '拖拽中' : '静止' }}</p>
    </div>
  </div>
</template>

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

const position = ref({ x: 0, y: 0 })
const isDragging = ref(false)
const dragStart = ref({ x: 0, y: 0 })

const startDrag = (event) => {
  isDragging.value = true
  dragStart.value = {
    x: event.clientX - position.value.x,
    y: event.clientY - position.value.y
  }
  
  document.addEventListener('mousemove', onDrag)
  document.addEventListener('mouseup', stopDrag)
}

const onDrag = (event) => {
  if (isDragging.value) {
    position.value = {
      x: event.clientX - dragStart.value.x,
      y: event.clientY - dragStart.value.y
    }
  }
}

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

onUnmounted(() => {
  document.removeEventListener('mousemove', onDrag)
  document.removeEventListener('mouseup', stopDrag)
})
</script>

<style scoped>
.drag-container {
  position: relative;
  height: 400px;
  border: 2px dashed #ccc;
  margin: 20px 0;
  overflow: hidden;
}

.draggable-item {
  position: absolute;
  width: 100px;
  height: 100px;
  background-color: #007bff;
  color: white;
  display: flex;
  align-items: center;
  justify-content: center;
  border-radius: 8px;
  cursor: move;
  user-select: none;
  transition: transform 0.1s ease;
}

.draggable-item:hover {
  background-color: #0056b3;
}

.info {
  position: absolute;
  top: 10px;
  right: 10px;
  background: rgba(255, 255, 255, 0.9);
  padding: 10px;
  border-radius: 4px;
  font-size: 14px;
}
</style>

模态框

vue
<template>
  <div>
    <button @click="showModal = true" class="open-modal-btn">
      打开模态框
    </button>
    
    <div
      v-if="showModal"
      class="modal-overlay"
      @click.self="closeModal"
      @keyup.esc="closeModal"
      tabindex="0"
    >
      <div class="modal-content" @click.stop>
        <div class="modal-header">
          <h3>模态框标题</h3>
          <button @click="closeModal" class="close-btn">×</button>
        </div>
        
        <div class="modal-body">
          <p>这是模态框的内容。</p>
          <p>按 ESC 键或点击外部区域可以关闭模态框。</p>
          
          <form @submit.prevent="handleSubmit">
            <input
              v-model="formData.name"
              placeholder="姓名"
              required
              class="form-input"
            />
            <input
              v-model="formData.email"
              type="email"
              placeholder="邮箱"
              required
              class="form-input"
            />
          </form>
        </div>
        
        <div class="modal-footer">
          <button @click="closeModal" class="btn btn-secondary">
            取消
          </button>
          <button @click="handleSubmit" class="btn btn-primary">
            确认
          </button>
        </div>
      </div>
    </div>
  </div>
</template>

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

const showModal = ref(false)
const formData = ref({
  name: '',
  email: ''
})

const closeModal = () => {
  showModal.value = false
  // 重置表单
  formData.value = {
    name: '',
    email: ''
  }
}

const handleSubmit = () => {
  if (formData.value.name && formData.value.email) {
    console.log('提交数据:', formData.value)
    closeModal()
  }
}

// 当模态框打开时,聚焦到模态框容器以便键盘事件生效
watch(showModal, async (newVal) => {
  if (newVal) {
    await nextTick()
    document.querySelector('.modal-overlay')?.focus()
  }
})
</script>

<style scoped>
.open-modal-btn {
  padding: 10px 20px;
  background-color: #007bff;
  color: white;
  border: none;
  border-radius: 4px;
  cursor: pointer;
  font-size: 16px;
}

.modal-overlay {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  background-color: rgba(0, 0, 0, 0.5);
  display: flex;
  align-items: center;
  justify-content: center;
  z-index: 1000;
  outline: none;
}

.modal-content {
  background: white;
  border-radius: 8px;
  max-width: 500px;
  width: 90%;
  max-height: 90vh;
  overflow-y: auto;
  box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
}

.modal-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 20px;
  border-bottom: 1px solid #eee;
}

.modal-header h3 {
  margin: 0;
}

.close-btn {
  background: none;
  border: none;
  font-size: 24px;
  cursor: pointer;
  color: #999;
  padding: 0;
  width: 30px;
  height: 30px;
  display: flex;
  align-items: center;
  justify-content: center;
}

.close-btn:hover {
  color: #333;
}

.modal-body {
  padding: 20px;
}

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

.modal-footer {
  display: flex;
  justify-content: flex-end;
  gap: 10px;
  padding: 20px;
  border-top: 1px solid #eee;
}

.btn {
  padding: 10px 20px;
  border: none;
  border-radius: 4px;
  cursor: pointer;
  font-size: 14px;
}

.btn-secondary {
  background-color: #6c757d;
  color: white;
}

.btn-primary {
  background-color: #007bff;
  color: white;
}

.btn:hover {
  opacity: 0.9;
}
</style>

性能优化

事件委托

对于大量相似元素的事件处理,使用事件委托可以提高性能:

vue
<template>
  <div @click="handleListClick" class="list-container">
    <div
      v-for="item in items"
      :key="item.id"
      :data-id="item.id"
      :data-action="item.action"
      class="list-item"
    >
      {{ item.name }}
      <button :data-action="'edit'" :data-id="item.id">编辑</button>
      <button :data-action="'delete'" :data-id="item.id">删除</button>
    </div>
  </div>
</template>

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

const items = ref([
  { id: 1, name: '项目 1', action: 'view' },
  { id: 2, name: '项目 2', action: 'view' },
  { id: 3, name: '项目 3', action: 'view' }
])

const handleListClick = (event) => {
  const target = event.target
  const action = target.dataset.action
  const id = target.dataset.id

  if (action && id) {
    switch (action) {
      case 'edit':
        console.log('编辑项目:', id)
        break
      case 'delete':
        console.log('删除项目:', id)
        items.value = items.value.filter(item => item.id !== parseInt(id))
        break
      case 'view':
        console.log('查看项目:', id)
        break
    }
  }
}
</script>

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

.list-item {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 10px;
  border: 1px solid #ddd;
  margin-bottom: 5px;
  border-radius: 4px;
  cursor: pointer;
}

.list-item:hover {
  background-color: #f5f5f5;
}

.list-item button {
  margin-left: 5px;
  padding: 5px 10px;
  border: none;
  border-radius: 3px;
  cursor: pointer;
  font-size: 12px;
}

.list-item button[data-action="edit"] {
  background-color: #28a745;
  color: white;
}

.list-item button[data-action="delete"] {
  background-color: #dc3545;
  color: white;
}
</style>

防抖和节流

对于频繁触发的事件,使用防抖和节流可以提高性能:

vue
<template>
  <div>
    <h3>防抖和节流示例</h3>
    
    <!-- 防抖搜索 -->
    <div class="section">
      <h4>防抖搜索 (500ms)</h4>
      <input
        v-model="searchTerm"
        @input="debouncedSearch"
        placeholder="输入搜索关键词..."
      />
      <p>搜索次数: {{ searchCount }}</p>
    </div>
    
    <!-- 节流滚动 -->
    <div class="section">
      <h4>节流滚动 (100ms)</h4>
      <div
        class="scroll-area"
        @scroll="throttledScroll"
      >
        <div class="scroll-content">
          <p v-for="i in 50" :key="i">滚动内容 {{ i }}</p>
        </div>
      </div>
      <p>滚动事件触发次数: {{ scrollCount }}</p>
    </div>
    
    <!-- 防抖按钮 -->
    <div class="section">
      <h4>防抖按钮 (1000ms)</h4>
      <button @click="debouncedSave">保存 (防抖)</button>
      <p>保存次数: {{ saveCount }}</p>
    </div>
  </div>
</template>

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

const searchTerm = ref('')
const searchCount = ref(0)
const scrollCount = ref(0)
const saveCount = ref(0)

// 防抖函数
const debounce = (func, delay) => {
  let timeoutId
  return (...args) => {
    clearTimeout(timeoutId)
    timeoutId = setTimeout(() => func.apply(null, args), delay)
  }
}

// 节流函数
const throttle = (func, delay) => {
  let lastCall = 0
  return (...args) => {
    const now = Date.now()
    if (now - lastCall >= delay) {
      lastCall = now
      func.apply(null, args)
    }
  }
}

// 搜索函数
const search = () => {
  searchCount.value++
  console.log('搜索:', searchTerm.value)
}

// 滚动函数
const scroll = () => {
  scrollCount.value++
  console.log('滚动事件触发')
}

// 保存函数
const save = () => {
  saveCount.value++
  console.log('保存数据')
}

// 创建防抖和节流版本
const debouncedSearch = debounce(search, 500)
const throttledScroll = throttle(scroll, 100)
const debouncedSave = debounce(save, 1000)
</script>

<style scoped>
.section {
  margin: 20px 0;
  padding: 20px;
  border: 1px solid #ddd;
  border-radius: 8px;
}

.section h4 {
  margin-top: 0;
}

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

.scroll-area {
  height: 200px;
  overflow-y: auto;
  border: 1px solid #ddd;
  border-radius: 4px;
  padding: 10px;
}

.scroll-content {
  height: 1000px;
}

.section button {
  padding: 10px 20px;
  background-color: #007bff;
  color: white;
  border: none;
  border-radius: 4px;
  cursor: pointer;
  font-size: 16px;
}

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

.section p {
  margin: 10px 0;
  color: #666;
}
</style>

选项式 API

在选项式 API 中,事件处理的用法基本相同:

vue
<template>
  <div>
    <button @click="handleClick">点击我</button>
    <input @keyup.enter="handleEnter" v-model="message" />
  </div>
</template>

<script>
export default {
  data() {
    return {
      message: ''
    }
  },
  methods: {
    handleClick(event) {
      console.log('按钮被点击', event)
    },
    handleEnter() {
      console.log('Enter 键被按下:', this.message)
    }
  }
}
</script>

最佳实践

1. 使用方法而不是内联表达式处理复杂逻辑

vue
<!-- 不推荐 -->
<button @click="count++; updateHistory(); saveToStorage()">
  复杂操作
</button>

<!-- 推荐 -->
<button @click="handleComplexOperation">
  复杂操作
</button>

<script setup>
const handleComplexOperation = () => {
  count.value++
  updateHistory()
  saveToStorage()
}
</script>

2. 合理使用事件修饰符

vue
<!-- 好的做法 -->
<form @submit.prevent="handleSubmit">
  <button type="submit">提交</button>
</form>

<!-- 避免在方法中手动调用 preventDefault -->
<form @submit="handleSubmitWithPrevent">
  <button type="submit">提交</button>
</form>

<script setup>
// 不推荐
const handleSubmitWithPrevent = (event) => {
  event.preventDefault()
  // 处理逻辑
}

// 推荐
const handleSubmit = () => {
  // 处理逻辑
}
</script>

3. 避免在模板中使用复杂表达式

vue
<!-- 不推荐 -->
<button @click="items.filter(item => item.active).forEach(item => item.process())">
  处理
</button>

<!-- 推荐 -->
<button @click="processActiveItems">
  处理
</button>

<script setup>
const processActiveItems = () => {
  items.value
    .filter(item => item.active)
    .forEach(item => item.process())
}
</script>

4. 正确清理事件监听器

vue
<script setup>
import { onMounted, onUnmounted } from 'vue'

let resizeHandler

onMounted(() => {
  resizeHandler = () => {
    console.log('窗口大小改变')
  }
  window.addEventListener('resize', resizeHandler)
})

onUnmounted(() => {
  if (resizeHandler) {
    window.removeEventListener('resize', resizeHandler)
  }
})
</script>

下一步

vue study guide - 专业的 Vue.js 学习平台