模板语法与指令详解
模板是Vue与用户对话的窗口。掌握模板语法,就掌握了Vue的"表达能力"。
在前两篇文章中,我们搭建了开发环境,理解了Vue实例与数据绑定。今天,让我们深入学习Vue模板语法——这是你与用户交互的核心技能。
📌 写作约定:本系列文章以 Vue 3
<script setup>语法糖 为主要讲解方式,这是Vue 3.2+官方推荐的写法。同时会顺带介绍Vue 2和Vue 3 Options API的写法作为对比,帮助大家理解演进过程和维护老项目。
一、模板语法基础:Vue的"表达能力"
Vue模板基于HTML,通过特殊的语法扩展了HTML的表达能力。你可以把它想象成HTML的"超能力版本"。
1.1 插值语法:最基础的数据展示
双大括号{{ }}是Vue最常用的插值语法,也叫"Mustache语法"(因为长得像胡子):
<template>
<!-- 基础插值 -->
<p>{{ message }}</p>
<!-- 表达式插值 -->
<p>{{ count + 1 }}</p>
<p>{{ isActive ? '激活' : '未激活' }}</p>
<p>{{ message.split('').reverse().join('') }}</p>
<!-- 调用方法 -->
<p>{{ formatDate(date) }}</p>
</template>
<script setup>
import { ref } from 'vue'
const message = ref('Hello Vue!')
const count = ref(10)
const isActive = ref(true)
const date = ref(new Date())
const formatDate = (d) => {
return d.toLocaleDateString('zh-CN')
}
</script>
注意事项:
- 插值只能包含表达式,不能包含语句(如
var a = 1) - 每个绑定只能包含单个表达式
<template>
<!-- ✅ 正确:表达式 -->
<p>{{ count + 1 }}</p>
<!-- ❌ 错误:这是语句,不是表达式 -->
<p>{{ var a = 1 }}</p>
<!-- ❌ 错误:流程控制语句 -->
<p>{{ if (ok) { return message } }}</p>
<!-- ✅ 正确:用三元表达式代替 -->
<p>{{ ok ? message : '默认值' }}</p>
</template>
1.2 原始HTML:v-html
双大括号会将数据解释为纯文本,而非HTML。如果需要渲染HTML,使用v-html:
<template>
<!-- 普通插值:HTML会被转义 -->
<p>{{ rawHtml }}</p>
<!-- 输出:<span style="color: red">红色文字</span> -->
<!-- v-html:渲染真实HTML -->
<p v-html="rawHtml"></p>
<!-- 输出:红色文字(红色) -->
</template>
<script setup>
const rawHtml = '<span style="color: red">红色文字</span>'
</script>
⚠️ 安全警告:
v-html可能导致XSS攻击,绝对不要在用户提交的内容上使用:
<script setup>
// ❌ 危险:用户输入可能包含恶意脚本
const userComment = '<script>alert("XSS攻击!")<\/script>'
// ✅ 安全:只渲染可信内容
const trustedContent = '<strong>官方公告</strong>'
</script>
<template>
<!-- ❌ 危险! -->
<div v-html="userComment"></div>
<!-- ✅ 安全 -->
<div v-html="trustedContent"></div>
</template>
1.3 属性绑定:v-bind
双大括号不能用在HTML属性中,需要使用v-bind:
<template>
<!-- 完整写法 -->
<img v-bind:src="imageUrl" v-bind:alt="imageAlt">
<!-- 简写(推荐) -->
<img :src="imageUrl" :alt="imageAlt">
<!-- 动态绑定多个属性 -->
<img v-bind="imageAttrs">
</template>
<script setup>
const imageUrl = '/logo.png'
const imageAlt = 'Vue Logo'
const imageAttrs = {
src: '/logo.png',
alt: 'Vue Logo',
width: 100,
height: 100
}
</script>
布尔型属性:
<template>
<!-- disabled为false时,属性会被移除 -->
<button :disabled="isDisabled">提交</button>
<!-- 以下写法等价 -->
<button disabled>提交</button>
<button :disabled="true">提交</button>
</template>
<script setup>
const isDisabled = ref(false)
</script>
动态属性名:
<template>
<!-- 动态属性名 -->
<button :[attrName]="attrValue">动态属性</button>
<!-- 等价于 <button data-id="123">动态属性</button> -->
</template>
<script setup>
const attrName = 'data-id'
const attrValue = '123'
</script>
二、条件渲染:v-if vs v-show
条件渲染让你能够根据条件决定是否显示某个元素。
2.1 v-if、v-else、v-else-if
v-if是"真正"的条件渲染——条件为假时,元素不会被渲染到DOM中:
<template>
<div>
<!-- v-if -->
<p v-if="score >= 90">优秀!</p>
<!-- v-else-if -->
<p v-else-if="score >= 60">及格</p>
<!-- v-else -->
<p v-else>不及格,继续努力!</p>
</div>
</template>
<script setup>
import { ref } from 'vue'
const score = ref(85)
</script>
在<template>上使用v-if:
<template>
<!-- 在template上使用,可以包裹多个元素 -->
<template v-if="isLoggedIn">
<h2>欢迎回来!</h2>
<p>今天是:{{ today }}</p>
<button @click="logout">退出登录</button>
</template>
</template>
<script setup>
import { ref } from 'vue'
const isLoggedIn = ref(true)
const today = new Date().toLocaleDateString()
const logout = () => {
isLoggedIn.value = false
}
</script>
2.2 v-show
v-show通过CSS的display属性控制显示/隐藏:
<template>
<!-- 元素始终存在于DOM中,只是切换display -->
<p v-show="isVisible">这段文字可以被切换显示</p>
<button @click="isVisible = !isVisible">
{{ isVisible ? '隐藏' : '显示' }}
</button>
</template>
<script setup>
import { ref } from 'vue'
const isVisible = ref(true)
</script>
2.3 v-if vs v-show 对比
| 特性 | v-if | v-show |
|---|---|---|
| 渲染方式 | 条件为假时销毁元素 | 始终渲染,CSS控制显示 |
| 切换开销 | 高(需要销毁/重建) | 低(只改CSS) |
| 初始渲染开销 | 低(条件为假时不渲染) | 高(始终渲染) |
| 适用场景 | 条件很少改变 | 需要频繁切换 |
选择建议:
- 运行时条件很少改变 → 用
v-if - 需要频繁切换显示/隐藏 → 用
v-show
<template>
<!-- ✅ 适合v-if:权限判断,很少改变 -->
<admin-panel v-if="isAdmin"></admin-panel>
<!-- ✅ 适合v-show:Tab切换,频繁切换 -->
<tab-content v-show="activeTab === 'content'"></tab-content>
</template>
2.4 v-if 与 v-for 的优先级
重要:不推荐同时使用v-if和v-for在同一元素上。
<template>
<!-- ❌ 不推荐:v-if优先级更高(Vue 3) -->
<li v-for="item in items" v-if="item.isActive" :key="item.id">
{{ item.name }}
</li>
<!-- ✅ 推荐:使用计算属性过滤 -->
<li v-for="item in activeItems" :key="item.id">
{{ item.name }}
</li>
<!-- ✅ 推荐:嵌套template -->
<template v-for="item in items" :key="item.id">
<li v-if="item.isActive">
{{ item.name }}
</li>
</template>
</template>
<script setup>
import { ref, computed } from 'vue'
const items = ref([
{ id: 1, name: 'Item 1', isActive: true },
{ id: 2, name: 'Item 2', isActive: false },
{ id: 3, name: 'Item 3', isActive: true }
])
const activeItems = computed(() =>
items.value.filter(item => item.isActive)
)
</script>
三、列表渲染:v-for
v-for可以遍历数组、对象、数字等,是Vue中最常用的指令之一。
3.1 遍历数组
<template>
<ul>
<!-- 基础用法 -->
<li v-for="item in items" :key="item.id">
{{ item.name }}
</li>
<!-- 带索引 -->
<li v-for="(item, index) in items" :key="item.id">
{{ index + 1 }}. {{ item.name }}
</li>
</ul>
</template>
<script setup>
import { ref } from 'vue'
const items = ref([
{ id: 1, name: '苹果' },
{ id: 2, name: '香蕉' },
{ id: 3, name: '橙子' }
])
</script>
3.2 遍历对象
<template>
<ul>
<!-- 只取值 -->
<li v-for="value in user" :key="value">
{{ value }}
</li>
<!-- 值和键 -->
<li v-for="(value, key) in user" :key="key">
{{ key }}: {{ value }}
</li>
<!-- 值、键、索引 -->
<li v-for="(value, key, index) in user" :key="key">
{{ index }}. {{ key }}: {{ value }}
</li>
</ul>
</template>
<script setup>
const user = {
name: '张三',
age: 25,
city: '北京'
}
</script>
3.3 遍历数字
<template>
<!-- 1 到 5 -->
<span v-for="n in 5" :key="n">{{ n }}</span>
<!-- 输出:1 2 3 4 5 -->
</template>
3.4 key的重要性
key是Vue识别节点的唯一标识,必须唯一且稳定:
<template>
<!-- ✅ 正确:使用唯一ID -->
<li v-for="item in items" :key="item.id">{{ item.name }}</li>
<!-- ❌ 错误:使用索引(可能导致渲染问题) -->
<li v-for="(item, index) in items" :key="index">{{ item.name }}</li>
<!-- ❌ 错误:key不唯一 -->
<li v-for="item in items" :key="item.type">{{ item.name }}</li>
</template>
为什么索引作为key有问题?
想象一个场景:你在列表开头插入一个新元素:
原来:[A, B, C] 索引:[0, 1, 2]
插入后:[D, A, B, C] 索引:[0, 1, 2, 3]
如果用索引作为key:
- 原来索引0对应A,现在对应D
- Vue会认为是A变成了D,而不是插入了新元素
- 这会导致状态错乱(如输入框的值错位)
3.5 列表更新检测
Vue 3的响应式系统可以检测到数组的变更方法:
<script setup>
import { ref } from 'vue'
const items = ref(['a', 'b', 'c'])
// ✅ 响应式方法(Vue 3都支持)
items.value.push('d') // 末尾添加
items.value.pop() // 末尾删除
items.value.unshift('x') // 开头添加
items.value.shift() // 开头删除
items.value.splice(1, 1, 'y') // 替换
items.value.sort() // 排序
items.value.reverse() // 反转
// ✅ 直接赋值(Vue 3支持)
items.value[0] = 'new'
// ✅ 修改长度(Vue 3支持)
items.value.length = 2
</script>
Vue 2的注意事项(已过时,了解即可):
- Vue 2不能检测
items[index] = newValue,需用Vue.set() - Vue 2不能检测
items.length = newLength
四、事件处理:v-on
v-on用于监听DOM事件,简写为@。
4.1 基本用法
<template>
<!-- 完整写法 -->
<button v-on:click="count++">点击了 {{ count }} 次</button>
<!-- 简写(推荐) -->
<button @click="count++">点击了 {{ count }} 次</button>
<!-- 调用方法 -->
<button @click="handleClick">点击我</button>
<!-- 传递参数 -->
<button @click="handleClick('hello')">传参</button>
<!-- 访问事件对象 -->
<button @click="handleClick($event)">获取事件</button>
<!-- 同时传参和事件对象 -->
<button @click="handleClick('hello', $event)">两者都要</button>
</template>
<script setup>
import { ref } from 'vue'
const count = ref(0)
const handleClick = (msg, event) => {
console.log(msg) // 'hello'
console.log(event) // MouseEvent对象
}
</script>
4.2 事件修饰符
Vue提供了事件修饰符,让事件处理更简洁:
<template>
<!-- .stop:阻止冒泡 -->
<div @click="handleOuter">
<button @click.stop="handleInner">阻止冒泡</button>
</div>
<!-- .prevent:阻止默认行为 -->
<form @submit.prevent="handleSubmit">
<button type="submit">提交</button>
</form>
<!-- .capture:使用捕获模式 -->
<div @click.capture="handleCapture">捕获模式</div>
<!-- .self:只当事件在该元素本身触发时才触发 -->
<div @click.self="handleSelf">只有点击自己才触发</div>
<!-- .once:只触发一次 -->
<button @click.once="handleOnce">只触发一次</button>
<!-- .passive:提升移动端滚动性能 -->
<div @scroll.passive="handleScroll">滚动区域</div>
<!-- 链式调用 -->
<button @click.stop.prevent="doSomething">组合使用</button>
</template>
修饰符速查表:
| 修饰符 | 作用 | 等价代码 |
|---|---|---|
.stop |
阻止冒泡 | event.stopPropagation() |
.prevent |
阻止默认行为 | event.preventDefault() |
.capture |
使用捕获模式 | - |
.self |
只当事件在该元素本身触发 | - |
.once |
只触发一次 | - |
.passive |
提升滚动性能 | { passive: true } |
4.3 按键修饰符
<template>
<!-- 按键别名 -->
<input @keyup.enter="submit" />
<input @keyup.tab="nextInput" />
<input @keyup.delete="deleteItem" />
<input @keyup.esc="cancel" />
<input @keyup.space="toggle" />
<input @keyup.up="moveUp" />
<input @keyup.down="moveDown" />
<input @keyup.left="moveLeft" />
<input @keyup.right="moveRight" />
<!-- 系统修饰键 -->
<input @keyup.ctrl="handleCtrl" />
<input @keyup.alt="handleAlt" />
<input @keyup.shift="handleShift" />
<input @keyup.meta="handleMeta" />
<!-- 组合使用 -->
<input @keyup.ctrl.enter="submit" />
<input @click.ctrl="doSomething" />
<!-- .exact:精确匹配 -->
<button @click.ctrl.exact="onlyCtrl">只按Ctrl</button>
<button @click.exact="noModifier">没有任何修饰键</button>
</template>
鼠标按钮修饰符:
<template>
<button @click.left="leftClick">左键</button>
<button @click.right="rightClick">右键</button>
<button @click.middle="middleClick">中键</button>
</template>
五、双向绑定:v-model
v-model是v-bind和v-on的语法糖,实现表单元素的双向数据绑定。
5.1 基本用法
<template>
<!-- 文本输入框 -->
<input v-model="text" placeholder="输入文字">
<p>你输入了:{{ text }}</p>
<!-- 多行文本 -->
<textarea v-model="content" placeholder="多行文本"></textarea>
<!-- 复选框 -->
<input type="checkbox" v-model="isChecked">
<label>是否同意:{{ isChecked }}</label>
<!-- 多个复选框 -->
<input type="checkbox" v-model="checkedNames" value="张三">
<input type="checkbox" v-model="checkedNames" value="李四">
<input type="checkbox" v-model="checkedNames" value="王五">
<p>选中:{{ checkedNames }}</p>
<!-- 单选框 -->
<input type="radio" v-model="picked" value="one">
<input type="radio" v-model="picked" value="two">
<p>选中:{{ picked }}</p>
<!-- 下拉框 -->
<select v-model="selected">
<option value="">请选择</option>
<option value="a">选项A</option>
<option value="b">选项B</option>
</select>
<p>选中:{{ selected }}</p>
<!-- 多选下拉框 -->
<select v-model="selectedMultiple" multiple>
<option value="a">A</option>
<option value="b">B</option>
<option value="c">C</option>
</select>
</template>
<script setup>
import { ref } from 'vue'
const text = ref('')
const content = ref('')
const isChecked = ref(false)
const checkedNames = ref([])
const picked = ref('')
const selected = ref('')
const selectedMultiple = ref([])
</script>
5.2 v-model修饰符
<template>
<!-- .lazy:在change事件后同步(而非input事件) -->
<input v-model.lazy="text">
<p>失去焦点后才更新:{{ text }}</p>
<!-- .number:自动转为数字 -->
<input v-model.number="age" type="number">
<p>类型:{{ typeof age }}</p>
<!-- .trim:自动去除首尾空格 -->
<input v-model.trim="name">
<p>去除空格后:{{ name }}</p>
<!-- 组合使用 -->
<input v-model.lazy.number="price">
</template>
<script setup>
import { ref } from 'vue'
const text = ref('')
const age = ref(0)
const name = ref('')
const price = ref(0)
</script>
5.3 v-model的原理
v-model本质上是v-bind和v-on的组合:
<template>
<!-- v-model -->
<input v-model="text">
<!-- 等价于 -->
<input
:value="text"
@input="text = $event.target.value"
>
</template>
自定义组件的v-model:
<template>
<!-- 父组件使用 -->
<CustomInput v-model="searchText" />
</template>
<!-- CustomInput.vue -->
<template>
<input
:value="modelValue"
@input="$emit('update:modelValue', $event.target.value)"
>
</template>
<script setup>
defineProps(['modelValue'])
defineEmits(['update:modelValue'])
</script>
六、其他指令
6.1 v-slot
v-slot用于插槽,简写为#:
<template>
<!-- 默认插槽 -->
<Card>
<template v-slot:default>
<p>卡片内容</p>
</template>
</Card>
<!-- 具名插槽 -->
<Card>
<template v-slot:header>
<h2>标题</h2>
</template>
<template v-slot:footer>
<p>页脚</p>
</template>
</Card>
<!-- 简写 -->
<Card>
<template #header>
<h2>标题</h2>
</template>
</Card>
<!-- 作用域插槽 -->
<List :items="items">
<template #default="{ item, index }">
<p>{{ index }}. {{ item.name }}</p>
</template>
</List>
</template>
6.2 v-pre
跳过编译,显示原始Mustache标签:
<template>
<!-- 正常编译 -->
<p>{{ message }}</p>
<!-- 输出:Hello -->
<!-- 跳过编译 -->
<p v-pre>{{ message }}</p>
<!-- 输出:{{ message }} -->
</template>
6.3 v-cloak
防止页面加载时闪烁未编译的模板:
<template>
<div v-cloak>
{{ message }}
</div>
</template>
<style>
[v-cloak] {
display: none;
}
</style>
6.4 v-once
只渲染一次,后续不再更新:
<template>
<!-- 只渲染一次,count变化不会更新 -->
<p v-once>{{ count }}</p>
<!-- 用于静态内容优化 -->
<div v-once>
<h1>网站标题</h1>
<p>这些内容永远不会更新</p>
</div>
</template>
七、自定义指令
当内置指令不够用时,可以创建自定义指令。
7.1 基本用法
Vue 3 <script setup> 写法:
<template>
<!-- 自动聚焦 -->
<input v-focus />
<!-- 防抖 -->
<input v-debounce:500="handleSearch" />
</template>
<script setup>
import { ref } from 'vue'
// =================== 自定义指令:自动聚焦 ===================
const vFocus = {
mounted(el) {
el.focus()
}
}
// =================== 自定义指令:防抖 ===================
const vDebounce = {
mounted(el, binding) {
const delay = binding.arg || 300 // 默认300ms
let timer = null
el.addEventListener('input', () => {
clearTimeout(timer)
timer = setTimeout(() => {
binding.value()
}, parseInt(delay))
})
}
}
const handleSearch = () => {
console.log('搜索...')
}
</script>
7.2 指令钩子函数
const myDirective = {
// 绑定元素的父组件挂载前
created(el, binding, vnode, prevVnode) {},
// 绑定元素的父组件挂载时
beforeMount(el, binding, vnode, prevVnode) {},
// 父组件挂载后
mounted(el, binding, vnode, prevVnode) {},
// 父组件更新前
beforeUpdate(el, binding, vnode, prevVnode) {},
// 父组件更新后
updated(el, binding, vnode, prevVnode) {},
// 父组件卸载前
beforeUnmount(el, binding, vnode, prevVnode) {},
// 父组件卸载后
unmounted(el, binding, vnode, prevVnode) {}
}
钩子参数:
// el:指令绑定的元素
// binding:包含以下属性
const binding = {
value: '传递的值', // v-my="value"
oldValue: '更新前的值', // 仅在 updated 中可用
arg: 'foo', // v-my:foo
modifiers: { bar: true }, // v-my.bar
instance: '组件实例',
dir: '指令定义对象'
}
7.3 实战案例:权限指令
<template>
<!-- 无权限时移除元素 -->
<button v-permission="'admin'">管理员按钮</button>
<button v-permission="'editor'">编辑按钮</button>
</template>
<script setup>
import { ref } from 'vue'
// 模拟当前用户权限
const currentRole = ref('editor')
// =================== 权限指令 ===================
const vPermission = {
mounted(el, binding) {
const requiredRole = binding.value
if (currentRole.value !== requiredRole) {
// 无权限时移除元素
el.parentNode?.removeChild(el)
}
}
}
</script>
7.4 实战案例:点击外部关闭
<template>
<div class="dropdown">
<button @click="show = !show">切换下拉</button>
<div v-if="show" v-click-outside="closeDropdown" class="menu">
<p>菜单项 1</p>
<p>菜单项 2</p>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
const show = ref(false)
const closeDropdown = () => {
show.value = false
}
// =================== 点击外部关闭指令 ===================
const vClickOutside = {
mounted(el, binding) {
el._clickOutside = (event) => {
// 点击的不是当前元素
if (!(el === event.target || el.contains(event.target))) {
binding.value()
}
}
document.addEventListener('click', el._clickOutside)
},
unmounted(el) {
document.removeEventListener('click', el._clickOutside)
}
}
</script>
<style scoped>
.dropdown {
position: relative;
display: inline-block;
}
.menu {
position: absolute;
top: 100%;
left: 0;
background: white;
border: 1px solid #ddd;
padding: 10px;
margin-top: 5px;
}
</style>
7.5 全局注册指令
// main.js
import { createApp } from 'vue'
import App from './App.vue'
const app = createApp(App)
// 全局注册指令
app.directive('focus', {
mounted(el) {
el.focus()
}
})
app.mount('#app')
对比Vue 2全局注册:
// Vue 2
Vue.directive('focus', {
inserted(el) { // Vue 2叫 inserted
el.focus()
}
})
八、指令速查表
| 指令 | 用途 | 简写 |
|---|---|---|
v-text |
更新元素的textContent | - |
v-html |
更新元素的innerHTML | - |
v-show |
切换display属性 | - |
v-if |
条件渲染 | - |
v-else |
否则分支 | - |
v-else-if |
否则如果 | - |
v-for |
列表渲染 | - |
v-on |
事件监听 | @ |
v-bind |
属性绑定 | : |
v-model |
双向绑定 | - |
v-slot |
插槽 | # |
v-pre |
跳过编译 | - |
v-cloak |
防止闪烁 | - |
v-once |
只渲染一次 | - |
v-memo |
缓存子树(Vue 3.2+) | - |
九、总结
今天我们全面学习了Vue模板语法和指令:
| 类别 | 核心内容 |
|---|---|
| 插值 | {{ }}、v-html、v-bind |
| 条件渲染 | v-if(销毁/重建)、v-show(CSS切换) |
| 列表渲染 | v-for遍历数组/对象/数字,key的重要性 |
| 事件处理 | v-on/@、事件修饰符、按键修饰符 |
| 双向绑定 | v-model、修饰符(.lazy/.number/.trim) |
| 自定义指令 | 钩子函数、权限指令、点击外部关闭 |
记住这些要点:
v-if是真正的条件渲染,v-show只是CSS切换v-for必须使用唯一的key- 事件修饰符让代码更简洁
v-model是语法糖,本质是:value+@input- 自定义指令适合直接操作DOM的场景
下一站预告
在下一篇文章《计算属性与侦听器》中,我们将深入学习:
- 计算属性的高级用法
- 侦听器的深度应用
- 性能优化技巧
敬请期待!
作者:洋洋技术笔记
发布日期:2026-03-01
系列:Vue.js从入门到精通 - 第3篇
评论区