【Vue3】交互控制:事件处理,侦听器和生命周期钩子

目录

    • 一、事件处理与表单交互
      • [1.1 事件绑定与修饰符](#1.1 事件绑定与修饰符)
      • [1.2 v-model ------ 双向数据绑定](#1.2 v-model —— 双向数据绑定)
      • [1.3 模板 ref ------ 获取真实 DOM](#1.3 模板 ref —— 获取真实 DOM)
    • 二、侦听器与生命周期
      • [2.1 watch ------ 侦听数据变化](#2.1 watch —— 侦听数据变化)
      • [2.2 watchEffect ------ 自动追踪依赖](#2.2 watchEffect —— 自动追踪依赖)
      • [2.3 生命周期钩子](#2.3 生命周期钩子)
      • [2.4 综合示例 ------ 搜索框防抖](#2.4 综合示例 —— 搜索框防抖)

一、事件处理与表单交互

1.1 事件绑定与修饰符

语法:

vue 复制代码
<!-- 基本事件绑定 -->
<元素 @事件名="处理函数">

<!-- 内联语句 -->
<元素 @事件名="表达式">

<!-- 传参 + 事件对象 -->
<元素 @事件名="函数($event, 其他参数)">

事件修饰符语法:

vue 复制代码
<元素 @事件名.修饰符="处理函数">

<!-- 可链式使用 -->
<元素 @事件名.修饰符1.修饰符2>

详细讲解:

之前学了基础的 @click,这里展开所有常用事件类型和修饰符。

常用事件类型:

vue 复制代码
<template>
  <!-- 鼠标事件 -->
  <button @click="handleClick">点击</button>
  <div @mousemove="handleMove">移动鼠标</div>
  <div @mouseenter="handleEnter">鼠标进入</div>
  <div @mouseleave="handleLeave">鼠标离开</div>
  <div @dblclick="handleDoubleClick">双击</div>

  <!-- 键盘事件 -->
  <input @keydown="handleKeyDown">
  <input @keyup="handleKeyUp">

  <!-- 表单事件 -->
  <input @focus="handleFocus" @blur="handleBlur">
  <input @input="handleInput">
  <select @change="handleChange">

  <!-- 窗口/文档事件(一般在 onMounted 中用 window.addEventListener 监听) -->
</template>

事件修饰符解决了"通用逻辑"的重复代码:

vue 复制代码
<template>
  <!-- .stop ------ 阻止事件冒泡 -->
  <!-- 点击内部按钮时,不会触发外层的 click -->
  <div @click="outerClick">
    <button @click.stop="innerClick">点我</button>
  </div>

  <!-- .prevent ------ 阻止默认行为 -->
  <!-- 提交表单时不刷新页面 -->
  <form @submit.prevent="onSubmit">

  <!-- .once ------ 事件只触发一次 -->
  <button @click.once="submitForm">提交(只能点一次)</button>

  <!-- .enter ------ 只在按回车时触发 -->
  <input @keyup.enter="search">

  <!-- .esc ------ 只在按 Esc 时触发 -->
  <div @keyup.esc="closeDialog">

  <!-- 链式使用 -->
  <form @submit.stop.prevent="handleSubmit">
  <!-- 既阻止冒泡,又阻止默认行为 -->

  <!-- 按键别名 -->
  <!-- .enter .tab .delete .esc .space .up .down .left .right -->
</template>

1.2 v-model ------ 双向数据绑定

语法:

vue 复制代码
<input v-model="响应式变量">
<textarea v-model="响应式变量">
<select v-model="响应式变量">
<input type="checkbox" v-model="响应式变量">
<input type="radio" v-model="响应式变量">

v-model 是 Vue 提供的一个语法糖,它等价于 :value="变量" @input="变量 = $event.target.value",帮你在模板和数据之间建立双向绑定。

就是从原来的响应式数据,到现在用户输入的同时数据也会变,数据变了那页面显示也会变,v-model实际上就是一次性做了这两件事而已。

详细讲解:

vue 复制代码
<script setup>
const username = ref('');
const password = ref('');
const gender = ref('male');
const hobbies = ref([]);
const city = ref('');
const agree = ref(false);
</script>

<template>
  <!-- 文本输入框 -->
  <input v-model="username" placeholder="用户名">

  <!-- 文本域 -->
  <textarea v-model="intro" placeholder="自我介绍"></textarea>

  <!-- 单选按钮(同一 name 的 radio 互斥) -->
  <input type="radio" v-model="gender" value="male"> 男
  <input type="radio" v-model="gender" value="female"> 女

  <!-- 多选框(数组收集选中值) -->
  <input type="checkbox" v-model="hobbies" value="reading"> 阅读
  <input type="checkbox" v-model="hobbies" value="coding"> 编程
  <input type="checkbox" v-model="hobbies" value="music"> 音乐

  <!-- 下拉选择 -->
  <select v-model="city">
    <option value="">请选择</option>
    <option value="beijing">北京</option>
    <option value="shanghai">上海</option>
  </select>

  <!-- 单个复选框(布尔值) -->
  <input type="checkbox" v-model="agree"> 我同意协议

  <!-- 修饰符 -->
  <input v-model.lazy="name">       <!-- .lazy:失去焦点时才同步,不是每输入一个字符都同步 -->
  <input v-model.number="age">      <!-- .number:自动转成数字类型 -->
  <input v-model.trim="search">     <!-- .trim:自动去掉首尾空格 -->
</template>

v-model 原理: 对于 <input>,v-model 等价于:

vue 复制代码
<!-- 这两行完全等价 -->
<input v-model="username">
<input :value="username" @input="username = $event.target.value">

所以本质上就是:数据 → 显示到输入框(:value),用户输入 → 更新数据(@input)。


1.3 模板 ref ------ 获取真实 DOM

语法:

vue 复制代码
<script setup>
import { ref, onMounted } from 'vue';

const 变量名 = ref(null);
// 类型标注(TS):const 变量名 = ref<HTMLElement | null>(null);
</script>

<template>
  <元素 ref="变量名">
</template>

ref 除了声明响应式数据,还能用在模板上------当用在 ref="变量" 时,它拿到的不是响应式值,而是真实的 DOM 元素组件实例

就是有时候会需要获取到真是的DOM元素来做一些操作,使用模板ref就可以获取到。

它需要在页面元素加载后才能操作,所以需要在onMounted里面写关于DOM的逻辑。

详细讲解:

vue 复制代码
<script setup>
import { ref, onMounted, nextTick } from 'vue';

// 模板 ref ------ 变量名和模板中的 ref 值对应
const inputRef = ref(null);
const divRef = ref(null);

onMounted(() => {
  // 组件挂载后,ref 才真正指向 DOM 元素
  console.log(inputRef.value);  // <input> DOM 元素

  // 可以直接操作原生 DOM
  inputRef.value.focus();
  inputRef.value.value = '自动填充';

  console.log(divRef.value.textContent);
});
</script>

<template>
  <input ref="inputRef" type="text">
  <div ref="divRef">这是一段文本</div>
</template>

何时需要模板 ref:

场景 说明
自动聚焦 页面加载后让输入框获得焦点
第三方库集成 需要传入一个原生 DOM 元素给非 Vue 库
获取元素尺寸 el.offsetHeightel.getBoundingClientRect()
操作 Canvas/Video 直接操作 <canvas><video> 元素
获取子组件实例 父组件需要调用子组件的方法

注意事项:

javascript 复制代码
// 1. 模板 ref 在 onMounted 之后才能访问
const el = ref(null);
console.log(el.value);       // ❌ null(还没挂载)
onMounted(() => {
  console.log(el.value);     // ✅ DOM 元素
});

// 2. v-for 中的 ref 会收集成数组
const items = ref([]);
// <li v-for="item in list" :ref="items">  →  items.value = [li, li, li]

// 3. 配合 nextTick 确保 DOM 更新后再操作
function updateAndFocus() {
  count.value++;
  nextTick(() => {
    inputRef.value.focus();  // DOM 更新完成后才执行
  });
}

二、侦听器与生命周期

2.1 watch ------ 侦听数据变化

语法:

javascript 复制代码
import { watch } from 'vue';

// 侦听单个数据源
watch(数据源, (新值, 旧值) => {
  // 当数据源变化时执行
});

// 侦听多个数据源
watch([数据源1, 数据源2], ([新1, 新2], [旧1, 旧2]) => {
  // 任一数据变化时执行
});

// 带选项
watch(数据源, 回调, {
  deep: true,       // 深度侦听对象内部变化
  immediate: true   // 立即执行一次回调(不等待变化)
});

详细讲解:

watch 用于在数据变化时执行副作用(发请求、操作 DOM、写入 localStorage 等)。

和computed的区别就是,computed是返回一个计算之后的响应式数据,而watch是监听某个东西的变化来执行一些操作。

javascript 复制代码
const keyword = ref('');
const user = ref({ name: 'Alice', age: 25 });
const page = ref(1);
const pageSize = ref(20);

// ===== 侦听 ref =====
watch(keyword, (newVal, oldVal) => {
  console.log(`搜索词从 "${oldVal}" 变为 "${newVal}"`);
  // 常用于:输入变化后重新请求搜索接口
  searchApi(newVal);
});

// ===== 侦听 reactive 对象的某个属性 =====
// 需要传一个 getter 函数
watch(
  () => user.value.name,
  (newVal) => {
    console.log(`用户名改为:${newVal}`);
  }
);

// ===== deep ------ 深度侦听 =====
// 侦听对象内部的变化(不传 deep,修改 user.value.name 不会触发)
watch(user, (newVal) => {
  console.log('用户信息变化了', newVal);
}, { deep: true });

// ===== immediate ------ 立即执行 =====
// 除了在变化时触发,初始化时也立即执行一次
watch(keyword, (newVal) => {
  fetchData(newVal);
}, { immediate: true });
// 等价于:页面加载时先请求一次,之后每次搜索词变化再请求

// ===== 侦听多个数据源 =====
watch([page, pageSize], ([newPage, newSize], [oldPage, oldSize]) => {
  console.log(`页码从 ${oldPage} 到 ${newPage},每页 ${newSize} 条`);
  fetchList({ page: newPage, pageSize: newSize });
});

// ===== 停止侦听 =====
// watch 返回一个停止函数
const stopWatch = watch(keyword, () => { /* ... */ });
// 在特定条件下停止侦听
onUnmounted(() => {
  stopWatch();  // 组件销毁时停止
});

watch 适用场景总结:

场景 示例
搜索框输入防抖请求 用户输入停止后再发请求
表单数据变化自动保存 数据变化时存到 localStorage
路由参数变化重新请求 watch(() => route.params.id, loadData)
多个数据联动 页码 + 每页条数变化时重新请求列表
表单校验 某个字段变化时校验另一个字段

2.2 watchEffect ------ 自动追踪依赖

语法:

javascript 复制代码
import { watchEffect } from 'vue';

watchEffect(() => {
  // 这里用到哪些响应式数据,就自动侦听哪些
  // 首次立即运行
});

watchEffectwatch 的区别:watch 需要你明确指定要侦听谁,watchEffect 自动追踪回调中用到的所有响应式数据。

详细讲解:

javascript 复制代码
const keyword = ref('');
const page = ref(1);
const pageSize = ref(20);
const data = ref([]);

// watch 写法 ------ 必须手动列出依赖
watch([keyword, page, pageSize], () => {
  fetchList({ keyword: keyword.value, page: page.value, pageSize: pageSize.value });
}, { immediate: true });

// watchEffect 写法 ------ 自动追踪用到了哪些 ref
watchEffect(() => {
  // 只要 keyword.value / page.value / pageSize.value 中任意一个变化
  // 这个函数就会重新执行
  fetchList({
    keyword: keyword.value,
    page: page.value,
    pageSize: pageSize.value
  });
});

清除副作用: 当侦听器重新执行前或组件销毁时,需要清理一些东西(如取消未完成的请求、清除定时器)。

javascript 复制代码
watchEffect((onCleanup) => {
  const timer = setTimeout(() => {
    console.log('执行搜索:', keyword.value);
  }, 500);

  // 在下一次重新运行前或组件卸载时,执行清理
  onCleanup(() => {
    clearTimeout(timer);
  });
});

watch vs watchEffect 怎么选:

watch watchEffect
依赖声明 手动指定 自动追踪
旧值新值 ✅ 能拿到 ❌ 拿不到
立即执行 immediate: true ✅ 默认就是
适用场景 需要旧值对比、多个值联动 只要数据一变就执行某件事

2.3 生命周期钩子

语法:

javascript 复制代码
import { onMounted, onUpdated, onUnmounted, onBeforeMount, onBeforeUpdate, onBeforeUnmount } from 'vue';

onMounted(() => {
  // 组件挂载到 DOM 后执行
});

onUnmounted(() => {
  // 组件销毁前执行(清理)
});

onUpdated(() => {
  // 组件因数据变化重新渲染后执行
});

详细讲解:

Vue 组件从创建到销毁会经历一系列阶段,每个阶段提供了钩子函数让你在特定时机执行代码。

复制代码
创建 → 挂载到 DOM → 更新 → 更新 → ... → 销毁

onBeforeMount
onMounted        ← 最常用:发请求、操作 DOM
onBeforeUpdate
onUpdated        ← 数据变化后、DOM 更新后
onBeforeUnmount
onUnmounted      ← 最常用:清理定时器、取消订阅
vue 复制代码
<script setup>
import { ref, onMounted, onUnmounted, onUpdated, nextTick } from 'vue';

const count = ref(0);
const data = ref(null);

// ===== onMounted ------ 组件挂载完成 =====
// 此时模板已经渲染到 DOM,可以获取 DOM 元素、发请求
onMounted(async () => {
  console.log('组件已挂载');
  // 1. 发请求获取数据
  const res = await api.getList();
  data.value = res;

  // 2. 操作 DOM(如果需要)
  // inputRef.value.focus();

  // 3. 添加非 Vue 的事件监听
  window.addEventListener('resize', handleResize);
});

// ===== onUnmounted ------ 组件销毁前 =====
// 清理在 onMounted 中设置的监听器、定时器等
onUnmounted(() => {
  console.log('组件即将销毁');
  window.removeEventListener('resize', handleResize);
  clearInterval(timer);
});

// ===== onUpdated ------ 组件更新后 =====
// 数据变化导致 DOM 重新渲染后触发
onUpdated(() => {
  console.log('组件已更新');
  // 注意:不要在这里修改响应式数据,会导致无限循环
});

// ===== 实际应用:实现一个倒计时 =====
const seconds = ref(60);
let timer;

onMounted(() => {
  timer = setInterval(() => {
    if (seconds.value > 0) {
      seconds.value--;
    }
  }, 1000);
});

onUnmounted(() => {
  clearInterval(timer);  // 组件销毁时必须清理,否则导致内存泄漏
});
</script>

<template>
  <p>倒计时:{{ seconds }} 秒</p>
</template>

生命周期的典型用途:

钩子 典型用途
onMounted 发 API 请求、操作 DOM、添加事件监听、初始化第三方库
onUnmounted 清理定时器、移除事件监听、取消订阅
onUpdated 数据更新后需要操作 DOM(不常用)
onBeforeMount 挂载前的准备工作(很少用)

2.4 综合示例 ------ 搜索框防抖

关于防抖和节流等知识会在之后的扩展里提到。

vue 复制代码
<script setup>
import { ref, watch, watchEffect, onMounted, onUnmounted } from 'vue';

const keyword = ref('');
const results = ref([]);
const loading = ref(false);

// 方式一:watch + 防抖
watch(keyword, (newVal) => {
  if (!newVal.trim()) {
    results.value = [];
    return;
  }
  loading.value = true;
  // 实际要用防抖处理,见扩展知识
  searchApi(newVal).then(res => {
    results.value = res;
    loading.value = false;
  });
});

// 方式二:watchEffect 自动追踪
watchEffect(async () => {
  // 这个函数自动追踪 keyword.value
  if (keyword.value) {
    loading.value = true;
    const res = await searchApi(keyword.value);
    results.value = res;
    loading.value = false;
  }
});
</script>

<template>
  <div>
    <input v-model="keyword" placeholder="搜索..." @keyup.esc="keyword = ''">
    <span v-if="loading">搜索中...</span>
    <ul v-if="results.length">
      <li v-for="item in results" :key="item.id">{{ item.name }}</li>
    </ul>
    <p v-else-if="keyword && !loading">未找到结果</p>
  </div>
</template>