侧边栏壁纸
博主头像
洋洋的技术笔记

行动起来,活在当下

  • 累计撰写 10 篇文章
  • 累计创建 2 个标签
  • 累计收到 0 条评论

目 录CONTENT

文章目录
VUE

03-模板语法与指令详解

洋洋的技术笔记
2026-09-18 / 0 评论 / 0 点赞 / 1 阅读 / 0 字 / 正在检测是否收录...

模板语法与指令详解

模板是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-ifv-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-modelv-bindv-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-bindv-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-htmlv-bind
条件渲染 v-if(销毁/重建)、v-show(CSS切换)
列表渲染 v-for遍历数组/对象/数字,key的重要性
事件处理 v-on/@、事件修饰符、按键修饰符
双向绑定 v-model、修饰符(.lazy/.number/.trim)
自定义指令 钩子函数、权限指令、点击外部关闭

记住这些要点

  1. v-if是真正的条件渲染,v-show只是CSS切换
  2. v-for必须使用唯一的key
  3. 事件修饰符让代码更简洁
  4. v-model是语法糖,本质是:value + @input
  5. 自定义指令适合直接操作DOM的场景

下一站预告

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

  • 计算属性的高级用法
  • 侦听器的深度应用
  • 性能优化技巧

敬请期待!


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

0

评论区