第1章 1.2 模板语法与指令
🎯 开场:为什么要学这个?
上一章我们搞定了 Vue 3 的开发环境,现在你已经有了一个能跑起来的 Vue 项目。
但你有没有这种感觉:看着别人写的 Vue 代码,里面一堆 v-if、v-for、v-model... 脑子里全是问号。
举个例子,你想让页面根据用户是否登录显示不同内容,结果折腾了半天发现——哎,用 v-if 就行?
这一章就是来解决这个问题的。
学完之后,你将能够:
- 读懂任何 Vue 组件模板里的"火星文"指令
- 写出能根据数据动态变化的页面
- 把上一章的"静态页面"变成"会动"的页面
🧱 基础:核心概念(小白视角)
1. 指令是什么?——模板里的"魔法咒语"
是什么?
指令就是 HTML 标签上的特殊标记,以 v- 开头。它们告诉 Vue:"这个元素要怎么响应数据变化"。
生活类比:
想象你是个指挥官,给士兵下达命令:
- 普通 HTML = 写在纸上的命令(死板的)
- 带指令的 HTML = 对着对讲机说的命令(活的,能根据情况变)
怎么用?
<p v-text="message"></p>
这行代码在说:"把这个 <p> 标签的文本内容,替换成 message 变量的值。"
2. v-text 和 v-html —— 文字显示的两把刀
是什么?
这两个指令控制标签里的文字内容。
为什么要用?
你想把 JavaScript 里的变量值显示到页面上?用它们就对了。
怎么用?
<template>
<div>
<!-- v-text:纯文本输出,像打字机一样 -->
<p v-text="plainText"></p>
<!-- v-html:解析 HTML,像打印机一样 -->
<div v-html="htmlContent"></div>
</div>
</template>
<script setup>
import { ref } from 'vue'
const plainText = ref('这是一段普通文字,<strong>加粗</strong>不会生效')
const htmlContent = ref('这段文字里 <strong>加粗</strong> 会真正生效')
</script>
输出效果:
这是一段普通文字,<strong>加粗</strong>不会生效
这段文字里 加粗 会真正生效
注意! v-html 有 XSS 风险,永远不要用它显示用户输入的内容!


3. v-show 和 v-if —— 显示/隐藏的两条路
是什么?
都是用来控制元素显示隐藏的,但原理不同。
为什么要用?
页面上很多元素不需要一直显示,比如弹窗、下拉菜单、错误提示...用它们来实现。
生活类比:
v-show= 把东西盖住(还在原地,只是看不见)v-if= 把东西搬走(根本不存在了)
怎么用?
<template>
<div>
<!-- v-show:用 CSS display:none 切换,显示频繁时用 -->
<p v-show="isVisible">我是用 v-show 显示的</p>
<!-- v-if:用条件渲染,切换成本高时用(组件初始化、权限切换) -->
<p v-if="hasPermission">我是用 v-if 显示的</p>
<button @click="toggle">点我切换</button>
</div>
</template>
<script setup>
import { ref } from 'vue'
const isVisible = ref(true)
const hasPermission = ref(true)
function toggle() {
isVisible.value = !isVisible.value
}
</script>
4. v-for —— 循环渲染的法宝
是什么?
让一个元素重复渲染N次。
为什么要用?
当你有一组数据要显示时,比如购物清单、用户列表、表格行...总不能写 N 个 <div> 吧?
生活类比:
v-for 就像复印机:给你一个模板,哗哗哗复制 N 份,每份填上不同的内容。
怎么用?
<template>
<div>
<h3>小明的购物清单</h3>
<ul>
<li v-for="(item, index) in shoppingList" :key="item.id">
{{ index + 1 }}. {{ item.name }} - ¥{{ item.price }}
</li>
</ul>
</div>
</template>
<script setup>
import { ref } from 'vue'
const shoppingList = ref([
{ id: 1, name: '牛奶', price: 5.5 },
{ id: 2, name: '面包', price: 8.0 },
{ id: 3, name: '鸡蛋', price: 12.0 }
])
</script>
输出:
1. 牛奶 - ¥5.5
2. 面包 - ¥8.0
3. 鸡蛋 - ¥12.0
敲黑板::key="item.id" 必须写! 用来告诉 Vue 每个节点的唯一身份,关系到性能和问题排查。
5. v-on —— 绑定事件的遥控器
是什么?
给元素绑定点击、输入等事件。
为什么要用?
页面要能响应用户操作啊!点按钮、填表单、滚动页面...全靠它。
怎么用?
<template>
<div>
<p>计数器的值:{{ count }}</p>
<!-- 完整写法 -->
<button v-on:click="increment">+1(完整写法)</button>
<!-- 简写:@click -->
<button @click="decrement">-1(简写)</button>
</div>
</template>
<script setup>
import { ref } from 'vue'
const count = ref(0)
function increment() {
count.value++
}
function decrement() {
count.value--
}
</script>
小贴士: 实际开发中用 @click 简写更香,省手指头。
6. v-bind —— 动态属性的桥梁
是什么?
把 JavaScript 变量的值,绑定到 HTML 属性的"管道"。
为什么要用?
HTML 属性的值经常需要动态变化,比如图片 src、链接 href、样式 class...
生活类比:
v-bind 就像翻译官:把 Vue 世界的话翻译成 HTML 世界的话。
怎么用?
<template>
<div>
<!-- 完整写法:v-bind:属性名 -->
<img v-bind:src="imageUrl" :alt="imageAlt">
<!-- 简写::属性名 -->
<a :href="pageUrl">{{ linkText }}</a>
<!-- 动态 class -->
<p :class="{ active: isActive, 'text-danger': hasError }">
这段文字样式在动态变化
</p>
</div>
</template>
<script setup>
import { ref } from 'vue'
const imageUrl = ref('https://picsum.photos/200')
const imageAlt = ref('示例图片')
const pageUrl = ref('https://example.com')
const linkText = ref('点击访问')
const isActive = ref(true)
const hasError = ref(false)
</script>
7. v-model —— 双向绑定的魔法
是什么?
把表单输入和数据变量绑在一起,一边变另一边也跟着变。
为什么要用?
做表单处理时太方便了!输入框、复选框、下拉框...再也不用手动 document.getElementById 了。
生活类比:
v-model 就像对讲机:你对着话筒说话,对方同时能听到,双方是同步的。v-bind 只能单向传达。
怎么用?
<template>
<div>
<p>你输入的内容:{{ userInput }}</p>
<!-- 双向绑定:输入框和数据自动同步 -->
<input v-model="userInput" placeholder="随便输入点什么">
<hr>
<p>是否同意协议:{{ agreed }}</p>
<label>
<input type="checkbox" v-model="agreed">
我已阅读并同意协议
</label>
</div>
</template>
<script setup>
import { ref } from 'vue'
const userInput = ref('')
const agreed = ref(false)
</script>
试着在输入框打字,你会看到上面的 <p> 标签同步更新!
🔥 实战:3 个递进的小项目
项目 1:动态名片展示(5 分钟)
目标: 用 v-bind 和 v-text 做一个能换肤的名片
<template>
<div class="business-card" :class="themeClass">
<h2 v-text="name"></h2>
<p class="title" v-text="title"></p>
<p class="email" v-text="email"></p>
<div class="theme-switcher">
<button @click="theme = 'light'">浅色主题</button>
<button @click="theme = 'dark'">深色主题</button>
<button @click="theme = 'blue'">蓝色主题</button>
</div>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
const name = ref('张小明')
const title = ref('全栈工程师')
const email = ref('zhangxiaoming@example.com')
const theme = ref('light')
const themeClass = computed(() => ({
'theme-light': theme.value === 'light',
'theme-dark': theme.value === 'dark',
'theme-blue': theme.value === 'blue'
}))
</script>
<style scoped>
.business-card {
padding: 20px;
border-radius: 10px;
margin: 20px;
transition: all 0.3s;
}
.theme-light { background: #f5f5f5; color: #333; }
.theme-dark { background: #2c3e50; color: #ecf0f1; }
.theme-blue { background: #3498db; color: white; }
.theme-switcher { margin-top: 15px; }
button { margin-right: 10px; padding: 5px 10px; cursor: pointer; }
</style>
预期输出: 点击不同按钮,名片背景色和文字颜色会切换
一句话解释: :class 绑定了一个计算属性,根据 theme 值切换不同的 CSS 类。
项目 2:待办清单(15 分钟)
目标: 用 v-for 渲染列表 + v-model 做输入 + v-if/v-show 处理空状态
<template>
<div class="todo-app">
<h1>小明的待办清单</h1>
<!-- 输入区域 -->
<div class="input-area">
<input
v-model="newTask"
@keyup.enter="addTask"
placeholder="输入新任务,按回车添加"
>
<button @click="addTask">添加</button>
</div>
<!-- 统计信息 -->
<p class="stats">
共 {{ tasks.length }} 项任务,已完成 {{ doneCount }} 项
</p>
<!-- 空状态提示 -->
<p v-if="tasks.length === 0" class="empty-tip">
清单是空的,添加一个任务吧!
</p>
<!-- 任务列表 -->
<ul class="task-list">
<li v-for="task in tasks" :key="task.id" :class="{ done: task.completed }">
<input
type="checkbox"
:checked="task.completed"
@change="toggleTask(task.id)"
>
<span v-text="task.text"></span>
<button class="delete-btn" @click="deleteTask(task.id)">删除</button>
</li>
</ul>
<!-- 批量操作 -->
<div v-show="tasks.length > 0" class="batch-actions">
<button @click="clearDone">清除已完成</button>
</div>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
const tasks = ref([
{ id: 1, text: '买菜', completed: false },
{ id: 2, text: '做饭', completed: true },
{ id: 3, text: '写代码', completed: false }
])
const newTask = ref('')
let nextId = 4
function addTask() {
if (!newTask.value.trim()) return
tasks.value.push({
id: nextId++,
text: newTask.value.trim(),
completed: false
})
newTask.value = ''
}
function toggleTask(id) {
const task = tasks.value.find(t => t.id === id)
if (task) task.completed = !task.completed
}
function deleteTask(id) {
tasks.value = tasks.value.filter(t => t.id !== id)
}
function clearDone() {
tasks.value = tasks.value.filter(t => !t.completed)
}
const doneCount = computed(() => tasks.value.filter(t => t.completed).length)
</script>
<style scoped>
.todo-app { max-width: 400px; margin: 30px auto; padding: 20px; }
.input-area { display: flex; gap: 10px; margin-bottom: 15px; }
input { flex: 1; padding: 8px; }
button { padding: 8px 15px; cursor: pointer; }
.stats { color: #666; font-size: 14px; }
.empty-tip { color: #999; font-style: italic; }
.task-list { list-style: none; padding: 0; }
.task-list li { padding: 10px; border-bottom: 1px solid #eee; display: flex; align-items: center; gap: 10px; }
.task-list li.done span { text-decoration: line-through; color: #999; }
.delete-btn { margin-left: auto; color: #e74c3c; }
</style>
预期输出:
- 页面加载显示 3 个预设任务
- 输入框输入内容按回车或点按钮添加新任务
- 勾选复选框切换完成状态
- 点删除移除任务
- 清除已完成按钮批量删除已完成任务
一句话解释: v-model 绑定输入框,v-for 渲染任务列表,v-if/v-show 处理空状态和批量操作按钮。
项目 3:天气小工具(15 分钟)
目标: 综合运用所有指令,做一个能切换城市查看天气的模拟工具
<template>
<div class="weather-app">
<h1>天气预报</h1>
<!-- 城市选择 -->
<div class="city-selector">
<select v-model="selectedCity">
<option v-for="city in cities" :key="city.id" :value="city.id">
{{ city.name }}
</option>
</select>
</div>
<!-- 天气卡片 -->
<div v-if="currentWeather" class="weather-card" :class="weatherClass">
<h2 v-text="currentWeather.cityName"></h2>
<p class="temperature">{{ currentWeather.temp }}°C</p>
<p class="condition" v-text="currentWeather.condition"></p>
<p class="wind">风力:{{ currentWeather.wind }}</p>
</div>
<!-- 穿搭建议 -->
<div v-show="currentWeather" class="suggestion">
<p>👕 穿搭建议:{{ clothingSuggestion }}</p>
</div>
<!-- 数据来源说明 -->
<p v-if="showSource" class="source">
数据更新时间:{{ updateTime }}
</p>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
// 模拟天气数据
const weatherData = {
beijing: { cityName: '北京', temp: 15, condition: '晴', wind: '3-4级' },
shanghai: { cityName: '上海', temp: 22, condition: '多云', wind: '2级' },
guangzhou: { cityName: '广州', temp: 28, condition: '雷阵雨', wind: '4级' },
chengdu: { cityName: '成都', temp: 18, condition: '阴', wind: '1级' }
}
const cities = [
{ id: 'beijing', name: '北京' },
{ id: 'shanghai', name: '上海' },
{ id: 'guangzhou', name: '广州' },
{ id: 'chengdu', name: '成都' }
]
const selectedCity = ref('beijing')
const showSource = ref(true)
const updateTime = ref(new Date().toLocaleString())
const currentWeather = computed(() => weatherData[selectedCity.value])
const weatherClass = computed(() => {
const temp = currentWeather.value.temp
if (temp < 10) return 'cold'
if (temp > 25) return 'hot'
return 'mild'
})
const clothingSuggestion = computed(() => {
const temp = currentWeather.value.temp
const condition = currentWeather.value.condition
if (temp < 10) return '厚外套、毛衣、秋裤'
if (temp < 20) return '薄外套、长袖、牛仔裤'
if (temp < 25) return '短袖、薄裤、舒适鞋'
return '轻薄夏装、防晒、遮阳帽' + (condition.includes('雨') ? ',记得带伞!' : '')
})
</script>
<style scoped>
.weather-app { max-width: 400px; margin: 30px auto; text-align: center; }
.city-selector { margin-bottom: 20px; }
select { padding: 10px 20px; font-size: 16px; }
.weather-card { padding: 30px; border-radius: 15px; color: white; margin-bottom: 20px; }
.weather-card.cold { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); }
.weather-card.mild { background: linear-gradient(135deg, #11998e 0%, #38ef7d 100%); }
.weather-card.hot { background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%); }
.temperature { font-size: 60px; font-weight: bold; margin: 10px 0; }
.condition { font-size: 20px; }
.suggestion { background: #f8f9fa; padding: 15px; border-radius: 10px; text-align: left; }
.source { color: #999; font-size: 12px; margin-top: 20px; }
</style>
预期输出:
- 下拉框选择不同城市
- 天气卡片动态显示对应城市的温度、天气状况、风力
- 卡片颜色根据温度变化(冷/适中/热)
- 穿搭建议根据温度和天气状况动态变化
一句话解释: 用 v-model 绑定城市选择,v-if 控制天气卡片显示,computed 计算穿搭建议。
💪 进阶:常见坑 + 性能小贴士
坑 1:v-for 忘了加 key
<!-- ❌ 错误:没有 key,Vue 会报警告 -->
<li v-for="item in items">{{ item.name }}</li>
<!-- ✅ 正确:加唯一的 key -->
<li v-for="item in items" :key="item.id">{{ item.name }}</li>
坑 2:v-model 绑定的值是基本类型却想修改
<!-- ❌ 错误:直接改 props 会报错 -->
<input v-model="propsValue">
<!-- ✅ 正确:要用 ref/reactive -->
<script setup>
const props = defineProps(['initialValue'])
const localValue = ref(props.initialValue)
</script>
<input v-model="localValue">
坑 3:v-if 和 v-for 一起用
<!-- ❌ 错误:v-for 优先级更高,会先循环再判断,浪费性能 -->
<li v-for="item in items" v-if="item.show">{{ item.name }}</li>
<!-- ✅ 正确:先用计算属性过滤 -->
<li v-for="item in visibleItems" :key="item.id">{{ item.name }}</li>
<script setup>
const visibleItems = computed(() => items.value.filter(i => i.show))
</script>
坑 4:v-html 用了用户输入的内容
<!-- ❌ 危险:XSS 攻击风险! -->
<div v-html="userInput"></div>
<!-- ✅ 安全:永远不要对用户输入用 v-html -->
<div>{{ userInput }}</div>
坑 5:@click 里的函数没写括号
<!-- ❌ 错误:事件对象会作为参数传入 -->
<button @click="handleClick">点击</button>
<!-- ✅ 正确:需要传参时写括号 -->
<button @click="handleClick(123)">点击</button>
<!-- ✅ 正确:同时需要事件对象 -->
<button @click="(e) => handleClick(123, e)">点击</button>
性能小贴士:使用 v-memo 优化列表渲染
对于大列表,当列表项不频繁更新时:
<!-- 只有 item.value 或 item.category 变化时才重新渲染 -->
<div v-for="item in list" v-memo="[item.value, item.category]" :key="item.id">
{{ item.name }}
</div>
调试技巧:Vue DevTools
浏览器装上 Vue DevTools 插件,可以:
- 查看组件的 data、computed
- 时间旅行调试(回看状态变化)
- 性能分析
// 代码里也可以手动打印调试
console.log('当前状态:', myVariable)
// 或者在模板里临时加一段观察
{{ console.log('变化了', myVariable) || '' }}
✏️ 练习题
练习 1(1 分钟):改颜色
题目: 把项目 1 的名片,theme 默认值从 'light' 改成 'blue'。
- 输入:无
- 预期输出:页面加载时直接显示蓝色主题
- 提示:
ref()里的初始值就是默认值
练习 2(2 分钟):加条件判断
题目: 在项目 1 名片里,如果名字是"张小明",在名字后面加个"👑"标识。
- 输入:无
- 预期输出:名字显示为"张小明👑"
- 提示:
:title属性里可以用三元表达式
练习 3(3 分钟):处理新数据
题目: 给项目 2 的待办清单添加一个"优先级"功能,每条任务有个 priority 属性(1=低,2=中,3=高),高优先级任务显示红色。
- 输入:添加
{ id: 4, text: '重要会议', completed: false, priority: 3 } - 预期输出:这条任务显示为红色文字
- 提示:用动态 class 绑定,根据 priority 值决定是否加
high-priority类
练习 4(4 分钟):串起两个项目
题目: 把项目 2 的待办清单改成:当任务数 ≥ 5 时,显示"任务较多,注意休息!"的提示。
- 输入:待办列表已有 5 条或更多任务
- 预期输出:列表上方显示警告提示
- 提示:用
v-show结合tasks.length >= 5条件
练习 5(5 分钟):看懂报错
题目: 以下代码运行时报错"TypeError: Cannot read property 'value' of undefined",请修复。
<script setup>
import { ref } from 'vue'
const userName = '张三' // 忘了 ref
</script>
<template>
<p>{{ userName.value }}</p> <!-- 读取方式也不对 -->
</template>
- 输入:上述代码
- 预期输出:页面正常显示"张三"
- 提示:需要两步修复:1. 用
ref()包装 2. 模板里不需要.value
作业:做一个「指令演练场」
需求描述:
做一个综合练习页面,把本文学到的所有指令都用一遍,加深理解。
功能点:
1. 数据绑定区:用 v-bind 动态切换图片,用 v-text/v-html 显示不同内容
2. 条件渲染区:用 v-if/v-show 实现选项卡切换
3. 列表渲染区:用 v-for 渲染一个排行榜数据(姓名、分数、排名)
4. 事件交互区:用 v-on(@) 绑定多个按钮,实现计数、切换等操作
5. 表单体验区:用 v-model 实现一个简单的用户信息表单(姓名、年龄、职业),实时显示填写内容
加分项:
1. 每个区域都有"清空/重置"按钮
2. 使用 CSS 让页面看起来更美观(不用框架,纯 CSS 即可)
验收标准:
- 能跑起来,没有报错
- 5 个区域都正常工作
- 代码有适量注释说明每个指令的作用
提交方式:
把代码贴到评论区,或者扔到 GitHub/Gitee 把链接发过来。
📚 总结 + 资源
本文学到的 3 个核心点
- 指令是模板的灵魂 —
v-开头的指令让 HTML 标签"活"起来,响应数据变化 - v-for 要配 :key — 列表渲染必加唯一 key,不然 Vue 会报警告,性能也会变差
- v-model 是双向绑定 — 表单输入用它最省事,一边改另一边自动同步
延伸学习资源
- Vue 3 官方文档 - 模板语法 — 最权威的参考
- Vue 3 官方文档 - 列表渲染 — 进阶内容看这里
- 《Vue.js 设计与实现》— 想深入原理的同学可以啃这本
互动钩子
你在实际项目里用过 v-model 吗?用它做过什么有意思的功能?
评论区聊聊你是怎么用的,踩过什么坑,老粉优先回复!👇
📢 下章预告:学会了模板语法和指令,但你有没有发现——我们的数据都是提前写死的?下一章我们来聊聊计算属性与侦听器,让数据自己会"思考",自动算出新结果!敬请期待~

评论(0)