Vue 3 模板语

在Vue 3的日常开发中,模板语法是我们接触频率最高的部分。很多开发者能熟练写出{``{ message }}和v-if,但遇到复杂场景时,常常会出现指令冲突、渲染逻辑不符合预期的问题。本文将从模板的底层逻辑出发,逐层拆解插值语法和指令语法的核心用法。

1.模板的理解

很多人把Vue模板等同于普通HTML,这其实是对模板最大的误解。Vue模板本质是一套声明式的HTML扩展语言,它的核心作用是让你用最贴近HTML的写法,描述"数据和视图之间的映射关系",而不需要手动操作DOM来完成更新。

1.1 什么是Vue模板

Vue模板是一种可以被Vue编译器解析的HTML代码,它在标准HTML的基础上扩展了插值、指令、属性绑定等语法。当你在.vue文件的<template>标签内编写代码时,这些代码并不会直接被浏览器渲染,而是会经过Vue的编译流程,转换为虚拟DOM的渲染函数,最终生成真实的DOM节点。

举一个基础的对比案例,用原生JS和Vue分别实现一个"点击数字自增"的功能:

javascript 复制代码
<!-- 原生JS实现 -->
<div>
  <button id="countBtn">0</button>
</div>
<script>
  let count = 0
  const btn = document.getElementById('countBtn')
  btn.addEventListener('click', () => {
    count++
    btn.textContent = count
  })
</script>

在原生写法中,你需要手动获取DOM元素、绑定事件、修改元素内容,当页面数据变多之后,代码容易变得混乱,维护成本高。

而用Vue模板实现同样的功能,代码会变得简洁:

TypeScript 复制代码
<template>
  <div>
    <button @click="count++">{{ count }}</button>
  </div>
</template>
<script setup>
import { ref } from 'vue'
const count = ref(0)
</script>

你不需要关心DOM怎么更新,只需要描述"点击按钮时count自增,页面上显示count的值",Vue会自动帮你完成所有DOM操作,这就是模板声明式编程的核心优势。

1.2模板的编译流程

  • 解析阶段:Vue的模板编译器会把模板字符串拆解成抽象语法树(AST),识别出里面的插值、指令、属性等特殊语法标记。

  • 优化阶段:编译器会遍历AST,给静态节点打上标记,这些节点后续数据变化时不会重新渲染,极大提升了更新性能。

  • 生成阶段:把优化后的AST转换为渲染函数(render function),执行渲染函数就能生成对应的虚拟DOM,最终通过diff算法对比更新真实DOM。

理解这个流程之后,你就能明白很多模板语法的底层逻辑:比如为什么v-if和v-for写在同一个标签上时优先级会和预期不同,为什么模板里不能直接写复杂的多行JavaScript语句。

1.3 模板的核心设计原则

Vue模板的设计始终遵循三个核心原:

  • 数据驱动视图:永远不要在模板里直接操作DOM,所有视图的变化都应该通过修改响应式数据来触发。
  • 声明式优先:你只需要描述"你想要的视图效果",不需要描述"一步步怎么实现这个效果"。
  • 语法轻量化:模板只负责做数据和视图的映射,复杂的业务逻辑应该抽离到script部分,不要在模板里堆砌大量复杂表达式。

2.插值语法

插值语法是Vue模板中最基础的数据绑定方式,它的作用是把响应式数据动态渲染到页面的指定位置。插值语法看似简单,但不同类型的插值有完全不同的特性,用错很容易带来安全隐患或者渲染性能问题。

2.1 文本插值:Mustache双大括号

双大括号{``{ }}是最常用的文本插值语法,它会自动把里面的内容转义为纯文本,再渲染到页面上。

最基础的用法直接绑定响应式变量:

TypeScript 复制代码
<template>
  <p>用户名:{{ username }}</p>
  <p>当前积分:{{ score }}</p>
</template>
<script setup>
import { ref } from 'vue'
const username = ref('张三')
const score = ref(1280)
</script>

当username或者score的值发生变化时,页面上对应的文本会自动更新,不需要任何额外操作。

除了直接绑定变量,双大括号内部还支持写单个JavaScript表达式,完成简单的计算和逻辑处理:

TypeScript 复制代码
<template>
  <!-- 数值运算 -->
  <p>下一级所需积分:{{ score + 100 }}</p>
  <!-- 三元表达式判断 -->
  <p>会员状态:{{ isVip ? 'VIP会员' : '普通用户' }}</p>
  <!-- 字符串方法处理 -->
  <p>用户名反转:{{ username.split('').reverse().join('') }}</p>
</template>
<script setup>
import { ref } from 'vue'
const score = ref(1280)
const isVip = ref(true)
const username = ref('张三')
</script>

这里一定要注意,双大括号内部只能写单个表达式,不能写多行语句、变量声明或者流程控制代码,比如下面这些写法都是错误的:

TypeScript 复制代码
<!-- 错误:不能写变量声明 -->
<p>{{ let a = 123 }}</p>
<!-- 错误:不能写if流程控制 -->
<p>{{ if(isVip) { return 'VIP' } }}</p>

如果遇到复杂的逻辑,你应该把它抽离成计算属性或者方法,再在模板里调用。

2.2 原始HTML插值:v-html指令

双大括号插值默认会对内容里的HTML标签进行转义,防止出现XSS攻击。如果你确实需要把一段HTML字符串作为真实的DOM元素渲染到页面,就需要使用v-html指令。

TypeScript 复制代码
<template>
  <!-- 普通双大括号会直接输出转义后的文本 -->
  <p>{{ richText }}</p>
  <!-- v-html会把字符串渲染为真实HTML -->
  <p v-html="richText"></p>
</template>
<script setup>
import { ref } from 'vue'
const richText = ref('<span style="color: red; font-weight: bold;">这是一段红色加粗的富文本内容</span>')
</script>

上面的代码中,第一个p标签会完整输出包含HTML标签的字符串,第二个p标签则会渲染出红色加粗的文本效果。

这里必须特别强调安全问题:绝对不要把用户输入、第三方接口返回的未经过滤的内容直接传给v-html,这会直接导致XSS跨站脚本攻击,恶意用户可以通过注入脚本窃取你的网站用户信息。v-html只能用于渲染你自己完全可控、提前经过安全过滤的富文本内容。

2.3 一次性渲染插值:v-once指令

默认情况下,所有插值都是响应式的,只要绑定的数据发生变化,对应的DOM内容就会自动更新。但有些内容是静态的,初始化渲染之后永远不会改变,这时候就可以用v-once指令,让这个元素只渲染一次,后续数据变化时不再更新,以此优化页面渲染性能。

TypeScript 复制代码
<template>
  <!-- 这个内容只会在初始化时渲染一次,后续timestamp变化也不会更新 -->
  <p v-once>页面首次加载时间:{{ timestamp }}</p>
  <!-- 普通插值会随着timestamp变化实时更新 -->
  <p>当前实时时间:{{ timestamp }}</p>
  <button @click="updateTime">更新时间</button>
</template>
<script setup>
import { ref } from 'vue'
const timestamp = ref(Date.now())
const updateTime = () => {
  timestamp.value = Date.now()
}
</script>

点击按钮之后,你会发现普通插值的时间会不断刷新,而带v-once的内容始终保持第一次渲染的结果。在包含大量静态内容的页面里合理使用v-once,可以有效减少Vue的响应式追踪开销,提升页面加载速度。

2.4 插值的常见避坑点

在使用插值语法时,有两个高频坑点需要特别注意:

  • 双大括号不能直接用在HTML属性里面,比如<div class="item {``{ active }}">这种写法是错误的,动态绑定属性必须使用v-bind指令。

  • 插值内部的表达式不要写得太复杂,比如不要把多层嵌套的数组遍历、多个条件判断直接写在双大括号里,这会让模板变得非常难维护,复杂逻辑一定要抽离到计算属性中。

3.指令语法

指令是Vue模板中带有v-前缀的特殊属性,是Vue实现条件渲染、列表渲染、事件处理、双向绑定等核心功能的基础。每个指令都有自己特定的作用域和使用规则,掌握它们的组合用法,才能写出高效易维护的模板代码。

3.1 数据绑定类指令

这类指令的核心作用是完成数据和DOM属性、状态的双向关联,是日常开发中使用频率最高的指令。

v-bind:动态属性绑定

v-bind的作用是把响应式数据动态绑定到HTML元素的属性上,它支持完整语法和简写语法,日常开发中推荐全部使用简写:,代码会更简洁。

最基础的单个属性绑定:

TypeScript 复制代码
<template>
  <!-- 完整语法 -->
  <img v-bind:src="avatarUrl" alt="用户头像">
  <!-- 简写语法 -->
  <img :src="avatarUrl" alt="用户头像">
  <a :href="userLink" target="_blank">访问用户主页</a>
  <button :disabled="isSubmitDisabled">提交表单</button>
</template>
<script setup>
import { ref } from 'vue'
const avatarUrl = ref('https://example.com/avatar.jpg')
const userLink = ref('https://example.com/user/123')
const isSubmitDisabled = ref(false)
</script>

v-bind最常用的场景是动态绑定class和style,支持对象和数组两种写法,非常灵活:

TypeScript 复制代码
<template>
  <!-- class对象写法:动态切换类名 -->
  <div class="base-card" :class="{ 'is-active': isActive, 'is-disabled': isDisabled }">卡片内容</div>
  <!-- class数组写法:绑定多个动态类名 -->
  <div :class=[cardType, cardTheme]>卡片内容</div>

  <!-- style对象写法:动态设置内联样式 -->
  <div :style="{ color: textColor, fontSize: fontSize + 'px', marginTop: '20px' }">动态样式文本</div>
  <!-- style数组写法:合并多个样式对象 -->
  <div :style=[baseStyle, customStyle]>合并样式的文本</div>
</template>
<script setup>
import { ref } from 'vue'
const isActive = ref(true)
const isDisabled = ref(false)
const cardType = ref('primary-card')
const cardTheme = ref('dark-theme')
const textColor = ref('#1890ff')
const fontSize = ref(16)
const baseStyle = ref({ padding: '16px', borderRadius: '8px' })
const customStyle = ref({ backgroundColor: '#f5f5f5' })
</script>

v-model:双向数据绑定

v-model是表单开发的核心指令,它本质是v-bind:value和v-on:input的语法糖,可以自动实现用户输入和响应式数据的双向同步,不需要手动处理事件监听。

不同表单元素的v-model基础用法:

XML 复制代码
<template>
  <!-- 文本输入框 -->
  <input v-model="username" placeholder="请输入用户名">
  <p>你输入的用户名:{{ username }}</p>

  <!-- 多行文本域 -->
  <textarea v-model="desc" placeholder="请输入个人简介"></textarea>

  <!-- 复选框 -->
  <input type="checkbox" v-model="agreeProtocol">
  <label>我已同意用户协议</label>

  <!-- 单选框组 -->
  <div>
    <input type="radio" v-model="gender" value="male"> 男
    <input type="radio" v-model="gender" value="female"> 女
  </div>

  <!-- 下拉选择框 -->
  <select v-model="selectedCity">
    <option value="guangzhou">广州</option>
    <option value="shenzhen">深圳</option>
    <option value="dongguan">东莞</option>
  </select>
</template>
<script setup>
import { ref } from 'vue'
const username = ref('')
const desc = ref('')
const agreeProtocol = ref(false)
const gender = ref('male')
const selectedCity = ref('dongguan')
</script>

v-model还提供了三个非常实用的修饰符,可以自动处理输入内容:

XML 复制代码
<template>
  <!-- .trim 自动去除输入内容的首尾空格 -->
  <input v-model.trim="searchKeyword" placeholder="搜索关键词">
  <!-- .number 自动把输入的内容转换为数字类型 -->
  <input v-model.number="userAge" type="number" placeholder="请输入年龄">
  <!-- .lazy 只有在输入框失焦时才更新数据,减少高频更新的性能开销 -->
  <input v-model.lazy="remark" placeholder="备注信息">
</template>

3.2 条件渲染类指令

条件渲染指令用于根据响应式数据的真假,动态控制DOM元素的渲染或者显示隐藏,是实现权限控制、状态切换、空数据占位的核心语法。

v-if vs v-show

v-if和v-show都能实现条件显示效果,但底层实现逻辑完全不同:

  • v-if是真正的条件渲染:当条件为false时,对应的DOM元素完全不会被渲染到页面上,组件会被销毁;当条件变为true时,才会重新创建DOM和组件。

  • v-show只是通过CSS的display: none来控制元素的显示隐藏,不管条件真假,DOM元素始终存在于页面中。

基础对比示例:

TypeScript 复制代码
<template>
  <!-- 条件为false时,这个div不会出现在DOM树里 -->
  <div v-if="isLogin">欢迎回来,尊敬的VIP用户</div>
  <!-- 条件为false时,这个div只是被隐藏,DOM始终存在 -->
  <div v-show="isLogin">欢迎回来,尊敬的VIP用户</div>
  <button @click="toggleLogin">切换登录状态</button>
</template>
<script setup>
import { ref } from 'vue'
const isLogin = ref(false)
const toggleLogin = () => {
  isLogin.value = !isLogin.value
}
</script>

实际开发中可以遵循:

  • 如果元素需要频繁切换显示隐藏,比如Tab切换、折叠面板、下拉菜单,优先用v-show,它的切换性能更好,不需要反复销毁重建DOM。

  • 如果元素的条件很少变化,比如权限判断、角色分支、空数据占位,优先用v-if,条件不满足时完全不渲染元素,可以节省初始渲染的性能开销。

v-else-if 和 v-else

这两个指令用来实现多分支的条件判断,使用时必须紧跟在v-if或者v-else-if的后面,不能单独使用。

示例代码,根据分数判断评级:

TypeScript 复制代码
<template>
  <div v-if="score >= 90">成绩评级:优秀</div>
  <div v-else-if="score >= 70">成绩评级:良好</div>
  <div v-else-if="score >= 60">成绩评级:及格</div>
  <div v-else>成绩评级:不及格</div>
</template>
<script setup>
import { ref } from 'vue'
const score = ref(85)
</script>

3.3 列表渲染类指令:v-for

v-for用于遍历数组、对象或者数字,批量生成多个DOM元素,是实现表格、列表、下拉选项等场景的核心指令。

最基础的数组遍历用法,同时获取元素和索引:

TypeScript 复制代码
<template>
  <ul>
    <li v-for="(item, index) in userList" :key="item.id">
      第{{ index + 1 }}位用户:{{ item.name }},年龄{{ item.age }}岁
    </li>
  </ul>
</template>
<script setup>
import { ref } from 'vue'
const userList = ref([
  { id: 1, name: '张三', age: 22 },
  { id: 2, name: '李四', age: 25 },
  { id: 3, name: '王五', age: 28 }
])
</script>

使用v-for时一定要给元素绑定唯一的:key属性,绝对不能直接把index作为key。如果key不唯一,Vue在更新列表时会出现DOM复用错误,导致表单输入框串值、元素状态错乱等诡异问题。

除了遍历数组,v-for还可以遍历对象,同时获取值、键名和索引:

TypeScript 复制代码
<template>
  <div v-for="(value, key, index) in userInfo" :key="key">
    {{ index + 1 }}. {{ key }}:{{ value }}
  </div>
</template>
<script setup>
import { ref } from 'vue'
const userInfo = ref({
  name: '张三',
  age: 22,
  city: '东莞',
  job: '前端工程师'
})
</script>

另外,v-for还支持直接遍历整数,快速生成指定数量的重复元素:

TypeScript 复制代码
<template>
  <!-- 生成1到5的数字列表 -->
  <span v-for="n in 5" :key="n">{{ n }} </span>
</template>

不要把v-if和v-for写在同一个元素标签上 ,在Vue 3中v-if的优先级比v-for更高,这会导致v-if无法访问到v-for遍历出来的变量,完全不符合你的预期。正确的做法是把v-for写在外层的<template>标签上,把v-if写在内部的元素上:

TypeScript 复制代码
<!-- 错误写法:v-if访问不到item变量 -->
<li v-for="item in list" v-if="item.isShow">{{ item.name }}</li>

<!-- 正确写法 -->
<template v-for="item in list" :key="item.id">
  <li v-if="item.isShow">{{ item.name }}</li>
</template>

3.4 事件处理类指令:v-on

v-on用于监听DOM事件,执行对应的JavaScript逻辑,它的简写是@,日常开发中几乎都使用简写形式。

TypeScript 复制代码
<template>
  <!-- 完整语法 -->
  <button v-on:click="handleClick">普通点击</button>
  <!-- 简写语法 -->
  <button @click="addCount">点击计数</button>
  <!-- 直接在指令里写内联语句 -->
  <button @click="count++">直接自增</button>
</template>
<script setup>
import { ref } from 'vue'
const count = ref(0)
const handleClick = () => {
  alert('按钮被点击了')
}
const addCount = () => {
  count.value++
}
</script>

v-on提供了大量实用的事件修饰符,可以不用在方法里手动处理事件逻辑,代码会变得非常简洁:

TypeScript 复制代码
template>
  <!-- .prevent 自动阻止事件默认行为,这里不会触发a标签的页面跳转 -->
  <a href="https://example.com" @click.prevent="handleLinkClick">点击我不会跳转</a>
  <!-- .stop 自动阻止事件冒泡,点击内部按钮不会触发外层div的点击事件 -->
  <div @click="outerClick">
    <button @click.stop="innerClick">内部按钮</button>
  </div>
  <!-- .once 事件只会触发一次,之后点击不再执行 -->
  <button @click.once="onlyClickOnce">只能点一次</button>
  <!-- .enter 监听键盘回车事件,输入框按回车触发搜索 -->
  <input @keyup.enter="handleSearch" v-model="keyword">
</template>
相关推荐
IT_陈寒5 分钟前
Vue的watch把我坑惨了,原来要这样用才靠谱
前端·人工智能·后端
用户76103044737114 分钟前
【转战智能眼镜开发】ROKID AIUI 小游戏 亚马逊标枪项目实战讲解
前端
风卿25 分钟前
看着AI写代码太孤独了,来欣赏星云吧:一个DSH 装饰插件
前端·javascript
牧艺34 分钟前
弹层不用再手算坐标了:CSS Anchor Positioning 从原理到可上线
前端·javascript·css
用户老李1 小时前
YOLO 目标检测自学教程 07:YOLOv3——多尺度预测
前端
moshuying1 小时前
ESP32 墨水屏金价牌(上):从点亮到把图片推过去
前端
李剑一1 小时前
别再抬杠,前端岗已经没了!无论前后端,我们最终都要成“全干”工程师
前端·面试
Highcharts.js1 小时前
Highcharts 仪表盘与速度仪图表库介绍
前端·信息可视化·仪表板·highcharts·图表开发·图表库·仪表可视化
Java小卷1 小时前
面向 Schema 编程,Vue 3 页面级渲染引擎!轻松玩转各场景 Validation!
vue.js·低代码