第 114 章:模板语法深入
学习目标
- 深入掌握 Vue 3 模板的各种指令
- 理解 v-if vs v-show 的差异
- 掌握 v-model 在表单中的各种用法
- 学会自定义指令与渲染函数
一、模板插值
1.1 文本插值
vue
<script setup>
const msg = 'hello';
</script>
<template>
<!-- 基本文本 -->
<p>{{ msg }}</p>
<!-- 单个 JS 表达式 -->
<p>{{ msg.toUpperCase() }}</p>
<p>{{ msg.split('').reverse().join('') }}</p>
<p>{{ Math.random() }}</p>
<!-- 三元表达式 -->
<p>{{ msg === 'hello' ? 'Hi' : 'Bye' }}</p>
<!-- 调用方法 -->
<p>{{ formatDate(new Date()) }}</p>
</template>1.2 原始 HTML(v-html)
vue
<script setup>
const rawHtml = '<span style="color: red">红色</span>';
</script>
<template>
<p v-html="rawHtml"></p>
</template>警告
永远不要对用户提供的内容使用 v-html,会导致 XSS 攻击。
二、属性绑定 v-bind
2.1 基础用法
vue
<script setup>
const url = 'https://vuejs.org';
const imgSrc = '/logo.png';
const isDisabled = false;
</script>
<template>
<!-- 完整 -->
<a v-bind:href="url">Link</a>
<!-- 简写 -->
<a :href="url">Link</a>
<!-- 动态参数名 -->
<a :[attrName]="url">Link</a>
<!-- 同一元素多个绑定 -->
<img :src="imgSrc" :alt="imgAlt" />
</template>2.2 绑定对象
vue
<script setup>
import { reactive } from 'vue';
const attrs = reactive({
id: 'container',
class: 'wrapper',
'data-test': 'demo'
});
</script>
<template>
<!-- 批量绑定属性 -->
<div v-bind="attrs">内容</div>
<!-- 等价于 -->
<div :id="attrs.id" :class="attrs.class">内容</div>
</template>2.3 class 与 style 绑定
vue
<script setup>
import { ref } from 'vue';
const isActive = ref(true);
const hasError = ref(false);
const classObject = ref({ active: true, 'text-danger': false });
const color = ref('red');
const fontSize = ref(16);
</script>
<template>
<!-- 对象语法 -->
<div :class="{ active: isActive, 'text-danger': hasError }"></div>
<!-- 数组语法 -->
<div :class="[isActive ? 'active' : '', 'base-class']"></div>
<!-- style 对象语法 -->
<div :style="{ color: color, fontSize: fontSize + 'px' }"></div>
<!-- style 数组语法 -->
<div :style="[baseStyles, overridingStyles]"></div>
</template>三、条件渲染
3.1 v-if 系列
vue
<script setup>
import { ref } from 'vue';
const type = ref('A');
const user = ref<{ name: string } | null>(null);
</script>
<template>
<!-- 基础用法 -->
<div v-if="type === 'A'">A</div>
<div v-else-if="type === 'B'">B</div>
<div v-else>C</div>
<!-- template 上使用(不会渲染 DOM) -->
<template v-if="user">
<h1>{{ user.name }}</h1>
<p>欢迎回来!</p>
</template>
</template>3.2 v-show
vue
<template>
<!-- 始终渲染,只是切换 display -->
<p v-show="isVisible">Hello</p>
</template>3.3 v-if vs v-show
| 维度 | v-if | v-show |
|---|---|---|
| 渲染 | 惰性:条件为真才渲染 | 始终渲染 |
| 切换 | 销毁/重建 DOM | 切换 display |
| 性能 | 切换开销大 | 初始开销大 |
| 适用 | 不常切换 | 频繁切换 |
| 支持 template | ✅ | ❌ |
四、列表渲染 v-for
4.1 基础用法
vue
<script setup>
const items = [
{ id: 1, name: 'Tom' },
{ id: 2, name: 'Jerry' }
];
const object = { a: 1, b: 2 };
const numbers = [1, 2, 3, 4, 5];
</script>
<template>
<!-- 数组 -->
<ul>
<li v-for="item in items" :key="item.id">
{{ item.name }}
</li>
</ul>
<!-- 带索引 -->
<ul>
<li v-for="(item, index) in items" :key="item.id">
{{ index + 1 }}. {{ item.name }}
</li>
</ul>
<!-- 对象 -->
<ul>
<li v-for="(value, key, index) in object" :key="key">
{{ index }}. {{ key }}: {{ value }}
</li>
</ul>
<!-- 数字范围 -->
<span v-for="n in 10" :key="n">{{ n }} </span>
</template>4.2 key 的作用
vue
<!-- ❌ 不推荐:用 index 当 key -->
<li v-for="(item, index) in items" :key="index">{{ item }}</li>
<!-- ✅ 推荐:用稳定的唯一 id -->
<li v-for="item in items" :key="item.id">{{ item.name }}</li>key 的作用
key 帮助 Vue 识别每个节点,高效复用和重排 DOM。
4.3 v-for 与 v-if 的优先级
vue
<!-- ⚠️ 不推荐:v-if 在 v-for 之前判断 -->
<li v-for="item in items" v-if="item.visible" :key="item.id">
<!-- ✅ 推荐:用 computed 过滤 -->
<script setup>
import { computed } from 'vue';
const items = [...];
const visibleItems = computed(() => items.filter(item => item.visible));
</script>
<template>
<li v-for="item in visibleItems" :key="item.id">
</template>五、事件处理
5.1 基础事件
vue
<script setup>
const count = ref(0);
const increment = (event: MouseEvent) => {
console.log(event.target);
count.value++;
};
const greet = (name: string, event?: MouseEvent) => {
console.log(`Hello, ${name}`, event);
};
</script>
<template>
<!-- 内联 -->
<button @click="count++">+</button>
<!-- 方法处理器 -->
<button @click="increment">+</button>
<!-- 传参 -->
<button @click="greet('Tom')">Greet</button>
<!-- 访问 event -->
<button @click="greet('Tom', $event)">Greet</button>
</template>5.2 事件修饰符
vue
<template>
<!-- .stop 阻止冒泡 -->
<button @click.stop="handleClick">Stop</button>
<!-- .prevent 阻止默认行为 -->
<form @submit.prevent="onSubmit">Submit</form>
<!-- .capture 捕获阶段触发 -->
<div @click.capture="handleCapture">Capture</div>
<!-- .self 仅当 event.target 是元素本身触发 -->
<div @click.self="handleSelf">Self</div>
<!-- .once 只触发一次 -->
<button @click.once="handleOnce">Once</button>
<!-- 键盘修饰符 -->
<input @keyup.enter="submit" />
<input @keyup.esc="cancel" />
<input @keyup.tab="next" />
<!-- 鼠标修饰符 -->
<button @click.left="handleLeft">Left</button>
<button @click.right.prevent="handleRight">Right</button>
</template>5.3 系统修饰键
vue
<template>
<!-- Ctrl + 点击 -->
<div @click.ctrl="handleCtrl">Ctrl</div>
<!-- Alt + Enter -->
<input @keyup.alt.enter="submit" />
<!-- Shift + 点击 -->
<div @click.shift="handleShift">Shift</div>
<!-- 精确修饰:只有指定键 -->
<button @click.ctrl.exact="handleOnlyCtrl">Only Ctrl</button>
</template>六、表单输入绑定 v-model
6.1 基础用法
vue
<script setup>
import { ref } from 'vue';
const text = ref('');
const textarea = ref('');
const checked = ref(false);
const picked = ref('A');
const selected = ref<string[]>([]);
const number = ref(0);
</script>
<template>
<!-- 文本 -->
<input v-model="text" />
<textarea v-model="textarea"></textarea>
<!-- 复选框 -->
<input type="checkbox" v-model="checked" />
<!-- 单选 -->
<input type="radio" v-model="picked" value="A" />
<input type="radio" v-model="picked" value="B" />
<!-- 选择框 -->
<select v-model="selected">
<option value="a">A</option>
<option value="b">B</option>
</select>
<!-- 多选 -->
<select v-model="selected" multiple>
<option value="1">一</option>
<option value="2">二</option>
</select>
</template>6.2 修饰符
vue
<template>
<!-- .lazy 在 change 事件后同步(而不是 input) -->
<input v-model.lazy="text" />
<!-- .number 自动转数字 -->
<input v-model.number="age" type="number" />
<!-- .trim 去除首尾空格 -->
<input v-model.trim="text" />
</template>6.3 自定义组件 v-model
vue
<!-- CustomInput.vue -->
<script setup lang="ts">
const props = defineProps<{ modelValue: string }>();
const emit = defineEmits<{ 'update:modelValue': [value: string] }>();
const update = (e: Event) => {
emit('update:modelValue', (e.target as HTMLInputElement).value);
};
</script>
<template>
<input :value="modelValue" @input="update" />
</template>
<!-- 父组件使用 -->
<CustomInput v-model="search" />6.4 多个 v-model
vue
<!-- UserForm.vue -->
<script setup lang="ts">
defineProps<{ name: string; age: number }>();
defineEmits<{
'update:name': [value: string];
'update:age': [value: number];
}>();
</script>
<template>
<input :value="name" @input="$emit('update:name', $event.target.value)" />
<input :value="age" type="number" @input="$emit('update:age', +$event.target.value)" />
</template>
<!-- 父组件 -->
<UserForm v-model:name="userName" v-model:age="userAge" />七、自定义指令
7.1 注册全局指令
typescript
// main.ts
import { createApp } from 'vue';
import App from './App.vue';
const app = createApp(App);
// 注册 v-focus 指令
app.directive('focus', {
mounted(el) {
el.focus();
}
});
app.mount('#app');7.2 注册局部指令
vue
<script setup lang="ts">
// v-focus 在模板中自动可用
const vFocus = {
mounted(el: HTMLElement) {
el.focus();
}
};
</script>
<template>
<input v-focus />
</template>7.3 钩子函数
typescript
const vDemo = {
// 创建时
created(el, binding, vnode) {},
// 挂载前
beforeMount(el, binding, vnode) {},
// 挂载后
mounted(el, binding, vnode) {
// el: 绑定的元素
// binding.value: 传入的值
// binding.arg: 指令参数(如 v-on:click 中的 click)
// binding.modifiers: 修饰符对象
},
// 更新前
beforeUpdate(el, binding, vnode, prevVnode) {},
// 更新后
updated(el, binding, vnode) {},
// 卸载前
beforeUnmount(el, binding, vnode) {},
// 卸载后
unmounted(el, binding, vnode) {}
};7.4 实战:防抖点击
typescript
// v-debounce-click.ts
import type { Directive } from 'vue';
interface ElType extends HTMLElement {
_debounceTimer?: number;
}
export const debounceClick: Directive<ElType, () => void> = {
mounted(el, binding) {
el.addEventListener('click', () => {
if (el._debounceTimer) clearTimeout(el._debounceTimer);
el._debounceTimer = window.setTimeout(() => {
binding.value();
}, 500);
});
},
unmounted(el) {
if (el._debounceTimer) clearTimeout(el._debounceTimer);
}
};vue
<template>
<button v-debounce-click="handleSubmit">提交</button>
</template>八、模板引用 ref
8.1 基础用法
vue
<script setup lang="ts">
import { ref, onMounted } from 'vue';
const inputRef = ref<HTMLInputElement | null>(null);
onMounted(() => {
inputRef.value?.focus();
});
</script>
<template>
<input ref="inputRef" />
</template>8.2 v-for 中的 ref
vue
<script setup lang="ts">
import { ref } from 'vue';
const list = ref([1, 2, 3]);
const itemRefs = ref<HTMLLIElement[]>([]);
</script>
<template>
<ul>
<li v-for="item in list" :key="item" ref="itemRefs">{{ item }}</li>
</ul>
</template>九、常见错误
9.1 模板中写语句
vue
<!-- ❌ 错误 -->
{{ var a = 1 }}
{{ if (ok) { return 'yes' } }}
<!-- ✅ 正确 -->
{{ a + 1 }}
{{ ok ? 'yes' : 'no' }}9.2 v-for 不加 key
vue
<!-- ❌ 会报警告 -->
<li v-for="item in items">{{ item }}</li>
<!-- ✅ 正确 -->
<li v-for="item in items" :key="item.id">{{ item }}</li>9.3 ref 在模板中漏 .value
vue
<script setup>
// 在 <script> 中需要 .value
count.value++;
// 在 <template> 中自动解包,不需要 .value
</script>
<template>
<p>{{ count }}</p> <!-- ✅ 不用 count.value -->
</template>十、本章小结
| 要点 | 关键 |
|---|---|
| 插值 | 文本 / v-html 原始 HTML |
| 属性绑定 | v-bind / :,支持对象/数组 |
| class/style | 对象/数组语法 |
| 条件渲染 | v-if 系列 / v-show |
| 列表渲染 | v-for + :key |
| 事件处理 | v-on / @,支持修饰符 |
| 表单 | v-model + .lazy/.number/.trim |
| 自定义指令 | app.directive() 或局部 vXxx |
动手练习
- 动态 class:实现一个按钮,点击切换 active 状态
- 表单:写一个登录表单,验证两次密码一致
- 自定义指令:实现
v-copy一键复制功能 - 多 v-model:写一个用户表单组件,支持 name 和 age 两个 v-model
推荐阅读
- 📖 Vue 3 模板语法 — 官方文档
- 📖 Vue 3 自定义指令 — 指令文档
- 🌐 Vue 3 Templates Cheatsheet — 速查表
下一章:第 115 章:响应式基础 →