模板语法与指令详解

0 阅读10分钟

如果说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-elsev-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-ifv-else-ifv-elsev-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-showv-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-ifv-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-forv-ifv-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回车
.tabTab
.deleteDelete/Backspace
.escEscape
.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

核心技术点

  1. 选择v-if还是v-show:频繁切换用v-show,条件很少变化用v-if
  2. v-for必须用key:使用唯一id,避免使用index
  3. 不要v-forv-if一起用:用计算属性过滤数据
  4. 事件修饰符链式调用:如@click.stop.prevent
  5. 表单验证组合:使用@blur失焦验证 + v-model实时绑定

下一站预告

在下一篇文章《计算属性与侦听器》中,我们将深入探讨:

  • 计算属性的缓存机制
  • 侦听器的深度监听和立即执行
  • 计算属性vs侦听器如何选择
  • 实战:搜索防抖与数据筛选

敬请期待!


作者:洋洋技术笔记
发布日期:2026-03-01
系列:Vue.js从入门到精通 - 第3篇

Vue模板语法与指令详解 | v-if、v-for、v-model、v-bind完整教程