如果说Vue的模板语法是一把瑞士军刀,那么指令就是刀上的各种功能。今天,让我们一起掌握这把神兵利器。
在上一篇文章中,我们学习了Vue实例和数据绑定的核心概念。今天,我们将深入Vue最强大的特性——模板语法与指令。这是Vue开发中最常用的知识,掌握它们,你就能像写HTML一样轻松构建动态界面。
一、模板语法基础:让HTML"活"起来
Vue的模板语法是一种扩展的HTML语法,它允许你在HTML中嵌入动态内容。Vue模板并不是真实的DOM,它是一种声明式的描述,Vue会将其编译为虚拟DOM。
1.1 插值表达式:最基础的模板语法
插值表达式是Vue模板中最基本的语法,使用双大括号{{ }}包裹:
<template>
<div>
<!-- 渲染文本 -->
<p>{{ message }}</p>
<!-- 渲染计算结果 -->
<p>1 + 1 = {{ 1 + 1 }}</p>
<!-- 渲染三元表达式 -->
<p>{{ isOnline ? '在线' : '离线' }}</p>
<!-- 渲染函数返回值 -->
<p>{{ formatDate(new Date()) }}</p>
</div>
</template>
<script setup>
import { ref } from 'vue'
// =================== 数据定义 ===================
const message = ref('你好,Vue!')
const isOnline = ref(true)
// =================== 方法定义 ===================
const formatDate = (date) => {
return `${date.getFullYear()}-${date.getMonth() + 1}-${date.getDate()}`
}
</script>
1.2 JavaScript表达式支持
在插值表达式中,你可以使用任何JavaScript表达式:
<template>
<div>
<!-- 数学运算 -->
<p>{{ price * quantity }}</p>
<!-- 字符串方法 -->
<p>{{ message.toUpperCase() }}</p>
<!-- 数组方法 -->
<p>{{ items.length }}</p>
<!-- 对象属性 -->
<p>{{ user.name }}</p>
<!-- 三元运算符 -->
<p>{{ status === 'active' ? '激活' : '未激活' }}</p>
</div>
</template>
<script setup>
import { ref } from 'vue'
// =================== 数据定义 ===================
const price = ref(100)
const quantity = ref(2)
const message = ref('hello')
const items = ref([1, 2, 3])
const user = ref({ name: '张三' })
const status = ref('active')
</script>
⚠️ 注意:插值表达式中只能使用表达式,不能使用语句:
<!-- ❌ 错误:这是语句,不是表达式 -->
<p>{{ const a = 1 }}</p>
<p>{{ if (true) { return 'yes' } }}</p>
<!-- ✅ 正确:使用表达式 -->
<p>{{ 1 + 1 }}</p>
<p>{{ true ? 'yes' : 'no' }}</p>
<p>{{ message || '默认值' }}</p>
二、指令系统概述:Vue的灵魂
指令(Directives)是Vue模板中最重要的特性。它们是带有特殊前缀v-的HTML属性,用于在模板中实现复杂的DOM操作。
2.1 指令的基本语法
指令的完整语法如下:
<!-- 基本指令 -->
<元素 v-指令名="表达式"></元素>
<!-- 带参数的指令 -->
<元素 v-指令名:参数="表达式"></元素>
<!-- 带修饰符的指令 -->
<元素 v-指令名:参数.修饰符="表达式"></元素>
2.2 常用指令一览
| 指令 | 作用 | 示例 |
|---|---|---|
v-text | 更新元素的文本内容 | v-text="message" |
v-html | 更新元素的HTML内容 | v-html="htmlContent" |
v-show | 根据条件切换元素显示/隐藏 | v-show="isVisible" |
v-if | 条件渲染 | v-if="isLoggedIn" |
v-else | v-if的else块 | v-else |
v-for | 列表渲染 | v-for="item in items" |
v-bind | 绑定属性 | v-bind:class="className" |
v-on | 绑定事件 | v-on:click="handleClick" |
v-model | 双向数据绑定 | v-model="inputValue" |
v-once | 只渲染一次 | v-once |
v-cloak | 解决闪烁问题 | v-cloak |
2.3 指令的语法糖
一些常用指令有简写形式:
<!-- v-bind 语法糖 -->
<img src="imageUrl" />
<!-- v-on 语法糖 -->
<button @click="handleClick">点击</button>
<!-- v-model 语法糖(本身就是语法糖) -->
<input v-model="inputValue">
三、条件渲染指令:让界面"会思考"
条件渲染指令根据表达式的真假值来决定是否渲染元素。Vue提供了v-if、v-else-if、v-else和v-show四个指令。
3.1 v-if / v-else-if / v-else
v-if是真正的条件渲染,它会根据条件决定是否创建/销毁元素:
<template>
<div>
<!-- 基础用法 -->
<p v-if="isLoggedIn">欢迎回来,{{ username }}!</p>
<!-- v-else -->
<p v-else>请登录</p>
<!-- v-else-if -->
<div v-if="role === 'admin'">
管理员面板
</div>
<div v-else-if="role === 'user'">
用户面板
</div>
<div v-else>
游客面板
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
// =================== 数据定义 ===================
const isLoggedIn = ref(true)
const username = ref('张三')
const role = ref('admin')
</script>
3.2 v-if vs v-show:如何选择?
v-show和v-if都能控制元素的显示隐藏,但原理不同:
<template>
<div>
<!-- v-if: 真正的条件渲染 -->
<!-- 条件为false时,元素不会创建 -->
<div v-if="show">v-if 显示</div>
<!-- v-show: CSS切换显示 -->
<!-- 条件为false时,元素仍存在,只是display:none -->
<div v-show="show">v-show 显示</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
// =================== 数据定义 ===================
const show = ref(true)
// =================== 切换显示 ===================
const toggle = () => {
show.value = !show.value
}
</script>
| 特性 | v-if | v-show |
|---|---|---|
| 原理 | 条件创建/销毁元素 | CSS display切换 |
| 切换开销 | 高(涉及DOM操作) | 低(仅CSS变化) |
| 初始渲染 | 条件为false时不渲染 | 始终渲染 |
| 适用场景 | 条件很少变化 | 条件频繁切换 |
选择建议:
- 元素显示状态频繁切换 → 用
v-show - 条件很少变化,或一次判断决定多个元素 → 用
v-if
3.3 key属性:帮助Vue区分元素
在使用v-if时,Vue默认会复用相同类型的元素以提高性能。但有时候我们不希望元素被复用,这时可以使用key属性:
<template>
<div>
<button @click="toggle">切换类型</button>
<!-- 添加key后,Vue会认为这是两个不同的元素 -->
<input v-if="type === 'text'" type="text" placeholder="文本输入" key="text-input">
<input v-else type="password" placeholder="密码输入" key="password-input">
</div>
</template>
<script setup>
import { ref } from 'vue'
// =================== 数据定义 ===================
const type = ref('text')
// =================== 切换方法 ===================
const toggle = () => {
type.value = type.value === 'text' ? 'password' : 'text'
}
</script>
实际应用:在登录表单切换"账号登录"和"短信登录"时,给不同类型的input添加不同的key,可以避免输入框内容残留。
3.4 实战:用户登录状态切换
<template>
<div class="login-demo">
<!-- 登录状态 -->
<div v-if="isLoggedIn" class="user-info">
<img src="user.avatar" />
<div class="user-detail">
<p class="username">{{ user.name }}</p>
<p class="role">{{ user.role }}</p>
</div>
<button @click="logout">退出登录</button>
</div>
<!-- 未登录状态 -->
<div v-else class="login-form">
<h3>欢迎登录</h3>
<input v-model="loginForm.username" placeholder="用户名">
<input v-model="loginForm.password" type="password" placeholder="密码">
<button @click="login">登录</button>
</div>
</div>
</template>
<script setup>
import { ref, reactive } from 'vue'
// =================== 数据定义 ===================
const isLoggedIn = ref(false)
const user = ref({
name: '管理员',
role: 'Administrator',
avatar: 'https://picsum.photos/100'
})
const loginForm = reactive({
username: '',
password: ''
})
// =================== 方法定义 ===================
const login = () => {
if (loginForm.username && loginForm.password) {
isLoggedIn.value = true
loginForm.username = ''
loginForm.password = ''
}
}
const logout = () => {
isLoggedIn.value = false
}
</script>
四、列表渲染指令:批量生成DOM
列表渲染指令v-for是Vue中最强大的指令之一,它可以根据数组或对象快速生成多个DOM元素。
4.1 v-for遍历数组
<template>
<ul>
<!-- 基本遍历 -->
<li v-for="item in items" :key="item.id">
{{ item.name }}
</li>
<!-- 同时获取索引 -->
<li v-for="(item, index) in items" :key="index">
{{ 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>
4.2 v-for遍历对象
<template>
<div>
<!-- 遍历对象属性值 -->
<p v-for="value in user" :key="value">
{{ value }}
</p>
<!-- 同时获取键和值 -->
<div v-for="(value, key) in user" :key="key">
{{ key }}: {{ value }}
</div>
<!-- 还可以获取索引 -->
<div v-for="(value, key, index) in user" :key="index">
{{ index }}. {{ key }}: {{ value }}
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
// =================== 数据定义 ===================
const user = ref({
name: '张三',
age: 25,
city: '北京'
})
</script>
4.3 key属性的重要性
强烈建议在使用v-for时提供key属性,它帮助Vue识别每个元素,实现高效的DOM更新:
<template>
<ul>
<!-- ✅ 推荐:使用唯一id作为key -->
<li v-for="item in items" :key="item.id">
{{ item.name }}
</li>
<!-- ⚠️ 不推荐:使用index作为key -->
<!-- 当数组顺序变化时,Vue可能会复用错误的元素 -->
<li v-for="(item, index) in items" :key="index">
{{ item.name }}
</li>
</ul>
</template>
4.4 v-for with v-if:小心使用
⚠️ 重要:不要在同一元素上同时使用v-for和v-if。v-if的优先级高于v-for,会导致逻辑混乱。
<!-- ❌ 错误写法:v-if无法访问item -->
<li v-for="item in items" v-if="item.active" :key="item.id">
{{ item.name }}
</li>
<!-- ✅ 正确写法:使用计算属性过滤 -->
<li v-for="item in activeItems" :key="item.id">
{{ item.name }}
</li>
// =================== 正确做法 ===================
const activeItems = computed(() => {
return items.value.filter(item => item.active)
})
4.5 实战:商品列表展示
<template>
<div class="product-list">
<h2>商品列表</h2>
<!-- 分类筛选 -->
<div class="filter">
<button
v-for="category in categories"
:key="category"
:class="{ active: currentCategory === category }"
@click="currentCategory = category"
>
{{ category }}
</button>
</div>
<!-- 商品列表 -->
<div class="products">
<div
v-for="product in filteredProducts"
:key="product.id"
class="product-card"
>
<img src="product.image" />
<h3>{{ product.name }}</h3>
<p class="price">¥{{ product.price }}</p>
<p class="stock" :class="{ 'low-stock': product.stock < 10 }">
库存: {{ product.stock }}
</p>
<button @click="addToCart(product)">加入购物车</button>
</div>
</div>
<!-- 空状态 -->
<p v-if="filteredProducts.length === 0" class="empty">
暂无商品
</p>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
// =================== 数据定义 ===================
const categories = ref(['全部', '电子产品', '服装', '食品'])
const currentCategory = ref('全部')
const products = ref([
{ id: 1, name: 'iPhone 15', category: '电子产品', price: 5999, stock: 50, image: 'https://picsum.photos/200' },
{ id: 2, name: 'MacBook Pro', category: '电子产品', price: 12999, stock: 20, image: 'https://picsum.photos/201' },
{ id: 3, name: '运动T恤', category: '服装', price: 199, stock: 100, image: 'https://picsum.photos/202' },
{ id: 4, name: '牛仔裤', category: '服装', price: 399, stock: 5, image: 'https://picsum.photos/203' },
{ id: 5, name: '有机苹果', category: '食品', price: 29, stock: 200, image: 'https://picsum.photos/204' }
])
// =================== 计算属性 ===================
const filteredProducts = computed(() => {
if (currentCategory.value === '全部') {
return products.value
}
return products.value.filter(p => p.category === currentCategory.value)
})
// =================== 方法定义 ===================
const addToCart = (product) => {
console.log(`已将 ${product.name} 加入购物车`)
product.stock--
}
</script>
五、事件处理指令:让界面"能动"
事件处理指令v-on用于监听DOM事件,并在事件触发时执行相应的处理函数。
5.1 v-on基础用法
<template>
<div>
<!-- 完整写法 -->
<button v-on:click="handleClick">点击</button>
<!-- 语法糖写法(推荐) -->
<button @click="handleClick">点击</button>
<!-- 传递参数 -->
<button @click="sayHello('张三')">打招呼</button>
<!-- 获取原生事件对象 -->
<button @click="handleEvent($event)">获取事件</button>
</div>
</template>
<script setup>
// =================== 方法定义 ===================
const handleClick = () => {
console.log('按钮被点击了')
}
const sayHello = (name) => {
console.log(`你好,${name}!`)
}
const handleEvent = (event) => {
console.log('事件对象:', event)
console.log('点击的元素:', event.target)
}
</script>
5.2 事件修饰符:让处理更优雅
Vue提供了丰富的事件修饰符,让事件处理更加便捷:
<template>
<div>
<!-- .stop - 阻止冒泡 -->
<div @click="parentClick">
<button @click.stop="childClick">阻止冒泡</button>
</div>
<!-- .prevent - 阻止默认行为 -->
<form @submit.prevent="submitForm">
<button type="submit">提交表单</button>
</form>
<!-- .capture - 使用捕获模式 -->
<div @click.capture="handleCapture">
<button @click="handleBubble">捕获模式</button>
</div>
<!-- .self - 只触发自身 -->
<div @click.self="handleSelf">
<button>只触发自身</button>
</div>
<!-- .once - 只触发一次 -->
<button @click.once="handleOnce">只触发一次</button>
<!-- .passive - 被动模式(提升滚动性能) -->
<div @wheel.passive="handleWheel">被动滚动</div>
</div>
</template>
<script setup>
// =================== 方法定义 ===================
const parentClick = () => console.log('父元素点击')
const childClick = () => console.log('子元素点击')
const submitForm = () => console.log('表单提交')
const handleCapture = () => console.log('捕获阶段')
const handleBubble = () => console.log('冒泡阶段')
const handleSelf = () => console.log('自身点击')
const handleOnce = () => console.log('只触发一次')
const handleWheel = () => console.log('滚动中')
</script>
修饰符链式调用:
<!-- 链式使用多个修饰符 -->
<button @click.stop.prevent="handleClick">链式修饰符</button>
5.3 按键修饰符:监听特定按键
<template>
<div>
<!-- 监听回车键 -->
<input @keyup.enter="handleEnter" placeholder="按回车提交">
<!-- 监听ESC键 -->
<input @keyup.esc="handleEsc" placeholder="按ESC清除">
<!-- 监听特定按键码 -->
<input @keyup.13="handleEnter" placeholder="13=回车">
<!-- 系统修饰符 + 按键 -->
<input @keyup.ctrl.enter="handleCtrlEnter" placeholder="Ctrl+回车">
<!-- .exact - 精确匹配修饰符 -->
<button @click.ctrl="handleCtrl">Ctrl+点击</button>
<button @click.ctrl.exact="handleCtrlOnly">只有Ctrl</button>
</div>
</template>
<script setup>
// =================== 方法定义 ===================
const handleEnter = () => console.log('回车提交')
const handleEsc = () => console.log('ESC清除')
const handleCtrlEnter = () => console.log('Ctrl+回车')
const handleCtrl = () => console.log('Ctrl+点击')
const handleCtrlOnly = () => console.log('只有Ctrl')
</script>
常用按键别名:
| 别名 | 按键 |
|---|---|
.enter | 回车 |
.tab | Tab |
.delete | Delete/Backspace |
.esc | Escape |
.space | 空格 |
.up | ↑ |
.down | ↓ |
.left | ← |
.right | → |
5.4 实战:计数器增强版
<template>
<div class="counter">
<h2>计数器</h2>
<div class="display">
<button @click="count--">-</button>
<span class="count">{{ count }}</span>
<button @click="count++">+</button>
</div>
<div class="info">
<p>历史记录:</p>
<ul>
<li v-for="(record, index) in history" :key="index">
{{ record }}
</li>
</ul>
</div>
<button @click="reset" class="reset">重置</button>
<!-- 快捷键提示 -->
<p class="tips">使用 ↑/↓ 键或 +/- 键调整数值</p>
</div>
</template>
<script setup>
import { ref, onMounted, onUnmounted } from 'vue'
// =================== 数据定义 ===================
const count = ref(0)
const history = ref([])
// =================== 键盘事件处理 ===================
const handleKeydown = (event) => {
if (event.key === 'ArrowUp' || event.key === '+') {
increment()
} else if (event.key === 'ArrowDown' || event.key === '-') {
decrement()
}
}
const increment = () => {
count.value++
addHistory(`+1 → ${count.value}`)
}
const decrement = () => {
count.value--
addHistory(`-1 → ${count.value}`)
}
const addHistory = (text) => {
history.value.unshift(text)
if (history.value.length > 10) {
history.value.pop()
}
}
const reset = () => {
count.value = 0
history.value = []
addHistory('重置')
}
// =================== 生命周期 ===================
onMounted(() => {
window.addEventListener('keydown', handleKeydown)
})
onUnmounted(() => {
window.removeEventListener('keydown', handleKeydown)
})
</script>
六、属性绑定指令:动态设置DOM
属性绑定指令v-bind用于动态绑定HTML属性,让属性的值可以动态变化。
6.1 v-bind基础用法
<template>
<div>
<!-- 完整写法 -->
<img src="imageUrl" />
<!-- 语法糖写法(推荐) -->
<img src="imageUrl" />
<!-- 绑定布尔属性 -->
<button :disabled="isDisabled">按钮</button>
<input :readonly="isReadonly">
<!-- 绑定多个属性 -->
<img v-bind="imageAttrs">
</div>
</template>
<script setup>
import { ref, reactive } from 'vue'
// =================== 数据定义 ===================
const imageUrl = ref('https://picsum.photos/200')
const altText = ref('图片')
const isDisabled = ref(false)
const isReadonly = ref(true)
// =================== 对象形式绑定多个属性 ===================
const imageAttrs = reactive({
src: 'https://picsum.photos/200',
alt: '图片',
title: '图片标题'
})
</script>
6.2 class和style的特殊处理
Vue对class和style绑定做了特殊处理,让动态样式更加方便:
<template>
<div>
<!-- 动态class:字符串形式 -->
<p :class="className">字符串形式</p>
<!-- 动态class:数组形式 -->
<p :class="[activeClass, errorClass]">数组形式</p>
<!-- 动态class:对象形式 -->
<p :class="{ active: isActive, 'text-danger': hasError }">对象形式</p>
<!-- 动态style:对象形式 -->
<p :style="{ color: textColor, fontSize: fontSize + 'px' }">style对象</p>
<!-- 动态style:数组形式 -->
<p :style="[baseStyle, overrideStyle]">style数组</p>
</div>
</template>
<script setup>
import { ref } from 'vue'
// =================== 数据定义 ===================
const className = ref('title')
const activeClass = ref('active')
const errorClass = ref('text-danger')
const isActive = ref(true)
const hasError = ref(false)
const textColor = ref('#42b983')
const fontSize = ref(18)
const baseStyle = ref({ color: 'blue' })
const overrideStyle = ref({ fontWeight: 'bold' })
</script>
6.3 实战:标签页切换
<template>
<div class="tabs">
<div class="tab-header">
<button
v-for="tab in tabs"
:key="tab.id"
:class="['tab-btn', { active: currentTab === tab.id }]"
@click="currentTab = tab.id"
>
{{ tab.label }}
</button>
</div>
<div class="tab-content">
<div
v-for="tab in tabs"
:key="tab.id"
v-show="currentTab === tab.id"
class="tab-pane"
>
{{ tab.content }}
</div>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
// =================== 数据定义 ===================
const currentTab = ref('home')
const tabs = ref([
{ id: 'home', label: '首页', content: '这是首页内容' },
{ id: 'about', label: '关于', content: '这是关于页面内容' },
{ id: 'contact', label: '联系', content: '这是联系我们内容' }
])
</script>
<style scoped>
.tab-header {
border-bottom: 2px solid #eee;
}
.tab-btn {
padding: 10px 20px;
border: none;
background: none;
cursor: pointer;
font-size: 16px;
transition: all 0.3s;
}
.tab-btn.active {
color: #42b983;
border-bottom: 2px solid #42b983;
margin-bottom: -2px;
}
.tab-pane {
padding: 20px;
}
</style>
七、双向绑定指令:表单的"灵魂"
v-model是Vue最强大的指令之一,它实现了表单输入和应用状态之间的双向绑定。
7.1 v-model原理
v-model本质上是以下两个操作的语法糖:
<!-- v-model 相当于 -->
<input :value="message" @input="message = $event.target.value">
<!-- v-model 写法 -->
<input v-model="message">
7.2 v-model修饰符
<template>
<div>
<!-- .lazy - 失焦时更新(性能优化) -->
<input v-model.lazy="lazyValue" placeholder="失焦后更新">
<p>{{ lazyValue }}</p>
<!-- .number - 自动转换为数字 -->
<input v-model.number="numberValue" type="text" placeholder="输入数字">
<p>类型: {{ typeof numberValue }}</p>
<!-- .trim - 自动去除首尾空格 -->
<input v-model.trim="trimValue" placeholder="去除空格">
<p>长度: {{ trimValue.length }}</p>
<!-- 组合使用 -->
<input v-model.lazy.number.trim="combinedValue" placeholder="组合修饰符">
</div>
</template>
<script setup>
import { ref } from 'vue'
// =================== 数据定义 ===================
const lazyValue = ref('')
const numberValue = ref(0)
const trimValue = ref('')
const combinedValue = ref('')
</script>
7.3 不同表单元素的v-model
<template>
<div class="form-demo">
<!-- 文本输入 -->
<div class="form-group">
<label>用户名:</label>
<input v-model="form.username" placeholder="请输入用户名">
</div>
<!-- 密码输入 -->
<div class="form-group">
<label>密码:</label>
<input v-model="form.password" type="password" placeholder="请输入密码">
</div>
<!-- 文本域 -->
<div class="form-group">
<label>简介:</label>
<textarea v-model="form.bio" placeholder="请输入简介"></textarea>
</div>
<!-- 复选框 -->
<div class="form-group">
<label>爱好:</label>
<label><input type="checkbox" v-model="form.hobbies" value="coding"> 编程</label>
<label><input type="checkbox" v-model="form.hobbies" value="music"> 音乐</label>
<label><input type="checkbox" v-model="form.hobbies" value="sports"> 运动</label>
</div>
<!-- 单选框 -->
<div class="form-group">
<label>性别:</label>
<label><input type="radio" v-model="form.gender" value="male"> 男</label>
<label><input type="radio" v-model="form.gender" value="female"> 女</label>
</div>
<!-- 下拉选择 -->
<div class="form-group">
<label>城市:</label>
<select v-model="form.city">
<option value="">请选择</option>
<option value="beijing">北京</option>
<option value="shanghai">上海</option>
<option value="guangzhou">广州</option>
</select>
</div>
<!-- 开关 -->
<div class="form-group">
<label>启用通知:</label>
<input type="checkbox" v-model="form.notify">
</div>
<!-- 表单数据展示 -->
<div class="preview">
<h3>表单数据:</h3>
<pre>{{ form }}</pre>
</div>
</div>
</template>
<script setup>
import { reactive } from 'vue'
// =================== 数据定义 ===================
const form = reactive({
username: '',
password: '',
bio: '',
hobbies: [],
gender: '',
city: '',
notify: false
})
</script>
7.4 实战:表单验证
<template>
<div class="login-form">
<h2>用户登录</h2>
<form @submit.prevent="handleSubmit">
<!-- 用户名 -->
<div class="form-item">
<input
v-model="form.username"
:class="{ error: errors.username }"
placeholder="用户名(4-16位)"
@blur="validateUsername"
>
<span v-if="errors.username" class="error-msg">{{ errors.username }}</span>
</div>
<!-- 邮箱 -->
<div class="form-item">
<input
v-model="form.email"
:class="{ error: errors.email }"
placeholder="请输入邮箱"
@blur="validateEmail"
>
<span v-if="errors.email" class="error-msg">{{ errors.email }}</span>
</div>
<!-- 密码 -->
<div class="form-item">
<input
v-model="form.password"
type="password"
:class="{ error: errors.password }"
placeholder="密码(6-18位)"
@blur="validatePassword"
>
<span v-if="errors.password" class="error-msg">{{ errors.password }}</span>
</div>
<!-- 确认密码 -->
<div class="form-item">
<input
v-model="form.confirmPassword"
type="password"
:class="{ error: errors.confirmPassword }"
placeholder="请再次输入密码"
@blur="validateConfirmPassword"
>
<span v-if="errors.confirmPassword" class="error-msg">{{ errors.confirmPassword }}</span>
</div>
<!-- 提交按钮 -->
<button type="submit" :disabled="!isValid">注册</button>
</form>
</div>
</template>
<script setup>
import { ref, reactive, computed } from 'vue'
// =================== 数据定义 ===================
const form = reactive({
username: '',
email: '',
password: '',
confirmPassword: ''
})
const errors = reactive({
username: '',
email: '',
password: '',
confirmPassword: ''
})
// =================== 验证方法 ===================
const validateUsername = () => {
if (!form.username) {
errors.username = '用户名不能为空'
} else if (form.username.length < 4 || form.username.length > 16) {
errors.username = '用户名长度需在4-16位之间'
} else {
errors.username = ''
}
}
const validateEmail = () => {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
if (!form.email) {
errors.email = '邮箱不能为空'
} else if (!emailRegex.test(form.email)) {
errors.email = '请输入正确的邮箱格式'
} else {
errors.email = ''
}
}
const validatePassword = () => {
if (!form.password) {
errors.password = '密码不能为空'
} else if (form.password.length < 6 || form.password.length > 18) {
errors.password = '密码长度需在6-18位之间'
} else {
errors.password = ''
}
}
const validateConfirmPassword = () => {
if (!form.confirmPassword) {
errors.confirmPassword = '请再次输入密码'
} else if (form.confirmPassword !== form.password) {
errors.confirmPassword = '两次密码输入不一致'
} else {
errors.confirmPassword = ''
}
}
// =================== 计算属性 ===================
const isValid = computed(() => {
return form.username &&
form.email &&
form.password &&
form.confirmPassword &&
!errors.username &&
!errors.email &&
!errors.password &&
!errors.confirmPassword
})
// =================== 提交方法 ===================
const handleSubmit = () => {
validateUsername()
validateEmail()
validatePassword()
validateConfirmPassword()
if (isValid.value) {
console.log('表单提交成功:', form)
alert('注册成功!')
}
}
</script>
八、其他常用指令:细节决定成败
除了上述主要指令,Vue还提供了一些实用的小指令。
8.1 v-text和v-html
<template>
<div>
<!-- v-text: 相当于 {{ }} 插值 -->
<p v-text="message"></p>
<!-- v-html: 渲染HTML内容 -->
<p v-html="htmlContent"></p>
</div>
</template>
<script setup>
import { ref } from 'vue'
// =================== 数据定义 ===================
const message = ref('这是普通文本')
const htmlContent = ref('<strong>这是加粗文本</strong> <a href="#">这是链接</a>')
</script>
⚠️ 注意:谨慎使用v-html,避免XSS攻击。只在可信内容上使用。
8.2 v-once:一次性渲染
使用v-once指令的元素只会渲染一次,之后的更新会被忽略:
<template>
<div>
<!-- 每次count变化都会更新 -->
<p>普通: {{ count }}</p>
<!-- 只渲染一次,之后不更新 -->
<p v-once>一次性: {{ count }}</p>
<button @click="count++">+1</button>
</div>
</template>
<script setup>
import { ref } from 'vue'
// =================== 数据定义 ===================
const count = ref(0)
</script>
应用场景:渲染静态内容(如网站底部版权信息)以提升性能。
8.3 v-cloak:解决闪烁问题
当Vue还未加载时,插值表达式{{ }}会原样显示在页面上。v-cloak可以隐藏未编译的模板:
<template>
<!-- 添加v-cloak属性 -->
<div v-cloak>
{{ message }}
</div>
</template>
<script setup>
import { ref } from 'vue'
// =================== 数据定义 ===================
const message = ref('你好')
</script>
<style>
/* 配合CSS隐藏未编译的模板 */
[v-cloak] {
display: none;
}
</style>
8.4 自定义指令:扩展Vue的能力
除了内置指令,Vue还允许我们创建自定义指令来实现更底层的DOM操作。自定义指令在实际开发中非常实用,比如图片懒加载、权限控制、输入格式化等。
<template>
<div>
<!-- 使用自定义指令 -->
<input v-focus placeholder="自动聚焦">
<div v-color="'red'">红色文字</div>
<div v-permission="'admin'">仅管理员可见</div>
</div>
</template>
<script setup>
// =================== 自定义指令 ===================
// 简单写法(Vue 3 <script setup>)
const vFocus = {
mounted: (el) => {
el.focus()
}
}
// 复杂写法
const vColor = {
mounted: (el, binding) => {
el.style.color = binding.value
}
}
// =================== 简易写法(了解一下即可) ===================
// 在Vue 3中,<script setup>中的以v开头的变量会自动成为自定义指令
</script>
自定义指令的应用场景:
- 输入框自动聚焦
- 元素权限控制
- 图片懒加载
- 拖拽排序
- 复制粘贴功能
💡 预告:自定义指令的详细用法将在系列《自定义指令开发》中深入讲解。
九、实战案例:任务清单应用
让我们用今天学到的知识,综合实现一个任务清单应用:
<template>
<div class="todo-app">
<h1>📝 我的任务清单</h1>
<!-- 添加任务 -->
<div class="add-task">
<input
v-model="newTask"
@keyup.enter="addTask"
placeholder="添加新任务..."
>
<button @click="addTask">添加</button>
</div>
<!-- 筛选tabs -->
<div class="tabs">
<button
v-for="tab in tabs"
:key="tab.value"
:class="{ active: currentTab === tab.value }"
@click="currentTab = tab.value"
>
{{ tab.label }}
<span class="count">({{ getTabCount(tab.value) }})</span>
</button>
</div>
<!-- 任务列表 -->
<ul class="task-list">
<li
v-for="task in filteredTasks"
:key="task.id"
:class="{ completed: task.completed }"
>
<input
type="checkbox"
v-model="task.completed"
>
<span class="task-text">{{ task.text }}</span>
<span class="task-date">{{ task.date }}</span>
<button class="delete-btn" @click="deleteTask(task.id)">×</button>
</li>
<!-- 空状态 -->
<li v-if="filteredTasks.length === 0" class="empty">
{{ currentTab === 'all' ? '暂无任务' : '暂无待办任务' }}
</li>
</ul>
<!-- 统计信息 -->
<div class="stats">
<span>总计: {{ tasks.length }}</span>
<span>已完成: {{ completedCount }}</span>
<span>完成率: {{ completionRate }}%</span>
<button @click="clearCompleted" v-if="completedCount > 0">清除已完成</button>
</div>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
// =================== 数据定义 ===================
const newTask = ref('')
const currentTab = ref('all')
const tabs = [
{ label: '全部', value: 'all' },
{ label: '待办', value: 'pending' },
{ label: '已完成', value: 'completed' }
]
const tasks = ref([
{ id: 1, text: '学习Vue模板语法', completed: true, date: '2026-03-01' },
{ id: 2, text: '完成实战案例', completed: false, date: '2026-03-02' },
{ id: 3, text: '整理学习笔记', completed: false, date: '2026-03-03' }
])
// =================== 计算属性 ===================
const filteredTasks = computed(() => {
switch (currentTab.value) {
case 'pending':
return tasks.value.filter(t => !t.completed)
case 'completed':
return tasks.value.filter(t => t.completed)
default:
return tasks.value
}
})
const completedCount = computed(() => {
return tasks.value.filter(t => t.completed).length
})
const completionRate = computed(() => {
if (tasks.value.length === 0) return 0
return Math.round((completedCount.value / tasks.value.length) * 100)
})
// =================== 方法定义 ===================
const addTask = () => {
const text = newTask.value.trim()
if (!text) return
tasks.value.push({
id: Date.now(),
text: text,
completed: false,
date: new Date().toISOString().split('T')[0]
})
newTask.value = ''
}
const deleteTask = (id) => {
const index = tasks.value.findIndex(t => t.id === id)
if (index > -1) {
tasks.value.splice(index, 1)
}
}
const getTabCount = (tab) => {
switch (tab) {
case 'pending':
return tasks.value.filter(t => !t.completed).length
case 'completed':
return tasks.value.filter(t => t.completed).length
default:
return tasks.value.length
}
}
const clearCompleted = () => {
tasks.value = tasks.value.filter(t => !t.completed)
}
</script>
<style scoped>
.todo-app {
max-width: 600px;
margin: 0 auto;
padding: 20px;
}
h1 {
text-align: center;
color: #42b983;
}
.add-task {
display: flex;
gap: 10px;
margin-bottom: 20px;
}
.add-task input {
flex: 1;
padding: 12px;
border: 2px solid #ddd;
border-radius: 8px;
font-size: 16px;
}
.add-task input:focus {
outline: none;
border-color: #42b983;
}
.add-task button {
padding: 12px 24px;
background: #42b983;
color: white;
border: none;
border-radius: 8px;
cursor: pointer;
}
.tabs {
display: flex;
gap: 10px;
margin-bottom: 20px;
}
.tabs button {
flex: 1;
padding: 10px;
border: none;
background: #f5f5f5;
border-radius: 8px;
cursor: pointer;
transition: all 0.3s;
}
.tabs button.active {
background: #42b983;
color: white;
}
.tabs .count {
font-size: 12px;
opacity: 0.8;
}
.task-list {
list-style: none;
padding: 0;
}
.task-list li {
display: flex;
align-items: center;
gap: 12px;
padding: 15px;
background: #f9f9f9;
margin-bottom: 8px;
border-radius: 8px;
transition: all 0.3s;
}
.task-list li.completed .task-text {
text-decoration: line-through;
color: #999;
}
.task-list li:hover {
transform: translateX(5px);
}
.task-text {
flex: 1;
}
.task-date {
font-size: 12px;
color: #999;
}
.delete-btn {
padding: 5px 10px;
background: #ff6b6b;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
.empty {
text-align: center;
color: #999;
padding: 40px;
}
.stats {
display: flex;
justify-content: space-between;
align-items: center;
margin-top: 20px;
padding: 15px;
background: #f5f5f5;
border-radius: 8px;
}
.stats button {
padding: 8px 16px;
background: #ff6b6b;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
</style>
十、总结
今天我们全面学习了Vue模板语法与指令的核心知识:
| 知识点 | 核心要点 |
|---|---|
| 插值表达式 | {{ }} 用于渲染文本,支持JavaScript表达式 |
| 条件渲染 | v-if/v-else/v-show,根据条件控制元素显示 |
| 列表渲染 | v-for 遍历数组/对象,务必使用key |
| 事件处理 | @click 绑定事件,修饰符.stop/.prevent等 |
| 属性绑定 | : 动态绑定HTML属性,class/style特殊处理 |
| 双向绑定 | v-model 实现表单输入同步,修饰符.lazy/.number/.trim |
| 其他指令 | v-text/v-html/v-once/v-cloak |
核心技术点
- 选择
v-if还是v-show:频繁切换用v-show,条件很少变化用v-if v-for必须用key:使用唯一id,避免使用index- 不要
v-for和v-if一起用:用计算属性过滤数据 - 事件修饰符链式调用:如
@click.stop.prevent - 表单验证组合:使用
@blur失焦验证 +v-model实时绑定
下一站预告
在下一篇文章《计算属性与侦听器》中,我们将深入探讨:
- 计算属性的缓存机制
- 侦听器的深度监听和立即执行
- 计算属性vs侦听器如何选择
- 实战:搜索防抖与数据筛选
敬请期待!
作者:洋洋技术笔记
发布日期:2026-03-01
系列:Vue.js从入门到精通 - 第3篇