目录
-
- 一、事件处理与表单交互
-
- [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.offsetHeight、el.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(() => {
// 这里用到哪些响应式数据,就自动侦听哪些
// 首次立即运行
});
watchEffect 和 watch 的区别: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>