Vue 模板语法
Vue 使用基于 HTML 的模板语法,允许你声明式地将 DOM 绑定到底层组件实例的数据。所有 Vue 模板都是语法上合法的 HTML,能被遵循规范的浏览器和 HTML 解析器解析。
在底层实现中,Vue 将模板编译成高度优化的 JavaScript 代码。结合响应式系统,当应用状态变化时,Vue 能够智能地推断出需要重新渲染的最少组件数量,并应用最少的 DOM 操作。
插值
文本插值
最基本的数据绑定形式是使用"Mustache"语法(双大括号)进行文本插值:
<script setup>
import { ref } from 'vue'
const message = ref('Hello Vue!')
</script>
<template>
<span>消息: {{ message }}</span>
</template>
Mustache 标签会被替换为相应组件实例中 message 属性的值。同时每次 message 属性更改时它也会同步更新。
一次性插值
如果你只想执行一次性插值,之后不再随数据变化更新,可以使用 v-once 指令:
<span v-once>这个将不会改变: {{ message }}</span>
v-once 会阻止该元素及其所有子节点的更新,可以用于优化性能。
原始 HTML
双大括号会将数据解释为纯文本,而不是 HTML。若想插入 HTML,需要使用 v-html 指令:
<script setup>
import { ref } from 'vue'
const rawHtml = '<span style="color: red">这是红色文字</span>'
</script>
<template>
<p>使用文本插值: {{ rawHtml }}</p>
<p>使用 v-html: <span v-html="rawHtml"></span></p>
</template>
渲染结果:
使用文本插值: <span style="color: red">这是红色文字</span>
使用 v-html: 这是红色文字(红色显示)
这里我们遇到了新概念。v-html 属性被称为指令。指令以 v- 作为前缀,表示它们是 Vue 提供的特殊属性。它们会在渲染的 DOM 上应用特殊的响应式行为。
span 的内容将会被替换为 rawHtml 属性的值,直接作为普通 HTML 插入——数据绑定会被忽略。注意,你不能使用 v-html 来组合局部模板,因为 Vue 不是基于字符串的模板引擎。反之,对于用户界面,组件更适合作为可重用和可组合的基本单位。
在网站上动态渲染任意 HTML 是非常危险的,容易导致 XSS 攻击。只对可信内容使用 v-html,永远不要在用户提交的内容上使用它。
Attribute 绑定
双大括号不能在 HTML 属性中使用。想要绑定属性,需要使用 v-bind 指令:
<script setup>
import { ref } from 'vue'
const dynamicId = 'my-id'
</script>
<template>
<div v-bind:id="dynamicId"></div>
</template>
v-bind 指令指示 Vue 将元素的 id 属性与组件的 dynamicId 属性保持同步。如果绑定的值是 null 或者 undefined,那么该属性将会从渲染的元素上移除。
简写
因为 v-bind 非常常用,它有专门的简写语法:
<!-- 完整语法 -->
<div v-bind:id="dynamicId"></div>
<!-- 简写 -->
<div :id="dynamicId"></div>
以 : 开头的属性可能看起来不太像正常的 HTML,但它是属性名称的合法字符,所有 Vue 支持的浏览器都能正确解析。此外,它们不会出现在最终的渲染标记中。
同名简写(Vue 3.4+)
如果属性的名称和绑定的 JavaScript 值的名称相同,可以进一步简化,省略属性值:
<!-- 等同于 :id="id" -->
<div :id></div>
<!-- 同样适用于完整语法 -->
<div v-bind:id></div>
这类似于 JavaScript 中声明对象时的属性简写语法。
布尔型属性
布尔型属性表示 true 或 false 值,通过元素上是否存在该属性来判断。例如 disabled 是最常见的布尔型属性之一。v-bind 在这种情况下工作方式略有不同:
<script setup>
import { ref } from 'vue'
const isButtonDisabled = ref(false)
</script>
<template>
<button :disabled="isButtonDisabled">按钮</button>
</template>
当 isButtonDisabled 为真值或空字符串时,disabled 属性会被包含在内。当 isButtonDisabled 为其他假值时,该属性将被忽略。
动态绑定多个属性
如果你有一个包含多个属性的 JavaScript 对象:
const objectOfAttrs = {
id: 'container',
class: 'wrapper',
style: 'background-color: green'
}
可以通过不带参数的 v-bind 将它们绑定到单个元素上:
<div v-bind="objectOfAttrs"></div>
使用 JavaScript 表达式
到目前为止,我们只在模板中绑定到了简单的属性键。但实际上,Vue 在所有数据绑定中都支持完整的 JavaScript 表达式:
<script setup>
import { ref } from 'vue'
const number = ref(10)
const ok = ref(true)
const message = ref('Hello')
const id = ref(1)
</script>
<template>
<!-- 数学运算 -->
<p>{{ number + 1 }}</p>
<!-- 三元表达式 -->
<p>{{ ok ? '是' : '否' }}</p>
<!-- 字符串方法 -->
<p>{{ message.split('').reverse().join('') }}</p>
<!-- 属性绑定中的表达式 -->
<div :id="`list-${id}`"></div>
</template>
这些表达式会在当前组件实例的数据作用域下作为 JavaScript 进行求值。
表达式限制
每个绑定只能包含一个表达式。表达式是可以被求值为值的代码片段。一个简单的判断方法是看它能否放在 return 后面。
以下示例是无效的:
<!-- 这是语句,不是表达式 -->
{{ var a = 1 }}
<!-- 流控制语句也不起作用,请使用三元表达式 -->
{{ if (ok) { return message } }}
调用函数
可以在绑定表达式中调用组件暴露的方法:
<script setup>
import { ref } from 'vue'
const date = ref(new Date())
function formatDate(date) {
return date.toLocaleDateString()
}
function toTitleDate(date) {
return date.toISOString()
}
</script>
<template>
<time :title="toTitleDate(date)" :datetime="date">
{{ formatDate(date) }}
</time>
</template>
绑定表达式中调用的函数将在每次组件更新时被调用,因此它们不应该有任何副作用,比如改变数据或触发异步操作。
受限的全局访问
模板表达式被沙箱化,只能访问有限的全局列表。该列表暴露了常用的内置全局变量,如 Math 和 Date。
没有显式包含在列表中的全局变量,例如用户在 window 上附加的属性,将无法在模板表达式中访问。你可以通过将其添加到 app.config.globalProperties 来显式地为所有 Vue 表达式定义其他全局变量。
指令
指令是带有 v- 前缀的特殊属性。Vue 提供了许多内置指令,包括我们上面介绍的 v-html 和 v-bind。
指令属性值预期是单个 JavaScript 表达式(v-for、v-on 和 v-slot 例外,稍后讨论)。指令的工作是当表达式的值变化时,响应式地更新 DOM。以 v-if 为例:
<p v-if="seen">现在你看到我了</p>
这里,v-if 指令会根据表达式 seen 的值的真假来移除或插入 <p> 元素。
指令参数
一些指令需要一个"参数",用冒号表示。例如,v-bind 指令用于响应式地更新 HTML 属性:
<a v-bind:href="url">链接</a>
<!-- 简写 -->
<a :href="url">链接</a>
这里 href 是参数,告诉 v-bind 指令将元素的 href 属性与表达式 url 的值绑定。
另一个例子是 v-on 指令,用于监听 DOM 事件:
<a v-on:click="doSomething">点击</a>
<!-- 简写 -->
<a @click="doSomething">点击</a>
参数是事件名称,这里是 click。v-on 对应的简写是 @ 字符。
动态参数
也可以在指令参数中使用 JavaScript 表达式,方法是使用方括号括起来:
<script setup>
import { ref } from 'vue'
const attributeName = 'href'
const url = 'https://vuejs.org'
const eventName = 'click'
function doSomething() {
alert('点击了!')
}
</script>
<template>
<!-- 动态属性名 -->
<a v-bind:[attributeName]="url">动态链接</a>
<!-- 简写形式 -->
<a :[attributeName]="url">动态链接</a>
<!-- 动态事件名 -->
<a v-on:[eventName]="doSomething">动态事件</a>
<!-- 简写形式 -->
<a @[eventName]="doSomething">动态事件</a>
</template>
这里的 attributeName 会作为一个 JavaScript 表达式被求值,其求值结果将作为最终的参数。例如,如果你的组件实例有一个数据属性 attributeName,其值为 "href",那么这个绑定将等价于 v-bind:href。
同样,你可以使用动态参数为一个处理函数绑定一个动态事件名称。
动态参数值的限制
动态参数预期会求出一个字符串,null 是一个特殊值,可以用于显式移除该绑定。任何其他非字符串的值都将触发警告。
动态参数语法的限制
动态参数表达式有一些语法约束,因为某些字符(如空格和引号)在 HTML 属性名称中无效。例如:
<!-- 这会触发编译器警告 -->
<a :['foo' + bar]="value">...</a>
如果你需要传入一个复杂的动态参数,最好使用计算属性替代。
当使用 DOM 内模板(直接写在 HTML 文件中的模板)时,应避免在名称中使用大写字符,因为浏览器会将属性名称强制转为小写:
<!-- DOM 内模板会被转换为 :[someattr] -->
<a :[someAttr]="value">...</a>
如果你的组件有 someAttr 属性而不是 someattr,代码将无法工作。单文件组件内的模板不受此限制。
修饰符
修饰符是以点开头的特殊后缀,表示指令需要以一些特殊方式绑定。例如,.prevent 修饰符告诉 v-on 指令对触发的事件调用 event.preventDefault():
<form @submit.prevent="onSubmit">...</form>
稍后你会看到 v-on 和 v-model 的其他修饰符示例。
指令语法图示
完整的指令语法如下:
v-指令名:参数.修饰符="表达式"
例如:v-bind:href.trim="url" 或简写 :href.trim="url"
内置指令速查
v-text
更新元素的文本内容:
<span v-text="message"></span>
<!-- 等同于 -->
<span>{{ message }}</span>
v-text 通过设置元素的 textContent 属性工作,因此会覆盖元素内现有的内容。如果需要更新部分文本内容,应该使用 Mustache 插值。
v-html
更新元素的 innerHTML:
<div v-html="htmlContent"></div>
在网站上动态渲染任意 HTML 是非常危险的,容易导致 XSS 攻击。只对可信内容使用 v-html,永远不要在用户提交的内容上使用它。
注意,在单文件组件中,scoped 样式不会应用在 v-html 内部的 HTML 上,因为该 HTML 不会被 Vue 的模板编译器处理。如果想让 scoped 样式作用于 v-html 内的内容,可以使用 CSS Modules 或额外的全局 <style> 元素。
v-show
根据表达式值的真假,切换元素的 display CSS 属性:
<h1 v-show="isVisible">Hello!</h1>
v-show 通过设置内联样式的 display 属性工作,并在元素可见时尝试尊重初始的 display 值。当条件变化时,它也会触发过渡效果。
v-if / v-else / v-else-if
根据表达式值的真假条件渲染元素:
<div v-if="type === 'A'">A</div>
<div v-else-if="type === 'B'">B</div>
<div v-else-if="type === 'C'">C</div>
<div v-else>Not A/B/C</div>
当 v-if 元素被切换时,元素及其包含的指令/组件会被销毁和重建。如果初始条件为假,则不会渲染内部内容。
v-if 和 v-for 一起使用时,v-if 的优先级更高。但不推荐在同一元素上同时使用它们,详情请参阅列表渲染章节。
v-for
基于源数据多次渲染元素或模板块:
<!-- 遍历数组 -->
<div v-for="item in items">{{ item.text }}</div>
<!-- 带索引 -->
<div v-for="(item, index) in items">{{ index }}: {{ item.text }}</div>
<!-- 遍历对象 -->
<div v-for="(value, key) in object">{{ key }}: {{ value }}</div>
<!-- 带索引的对象遍历 -->
<div v-for="(value, key, index) in object">{{ index }}. {{ key }}: {{ value }}</div>
<!-- 遍历数字 -->
<div v-for="n in 10">{{ n }}</div>
建议尽可能为 v-for 提供一个 key 属性,除非迭代的 DOM 内容足够简单:
<div v-for="item in items" :key="item.id">{{ item.name }}</div>
v-on(简写 @)
给元素绑定事件监听器:
<!-- 方法处理器 -->
<button v-on:click="doThis"></button>
<!-- 动态事件 -->
<button v-on:[event]="doThis"></button>
<!-- 内联语句 -->
<button v-on:click="doThat('hello', $event)"></button>
<!-- 简写 -->
<button @click="doThis"></button>
<!-- 简写动态事件 -->
<button @[event]="doThis"></button>
<!-- 修饰符 -->
<button @click.stop="doThis"></button>
<button @click.prevent="doThis"></button>
<!-- 无表达式阻止默认行为 -->
<form @submit.prevent></form>
<!-- 链式修饰符 -->
<button @click.stop.prevent="doThis"></button>
<!-- 按键修饰符 -->
<input @keyup.enter="onEnter" />
<!-- 只触发一次 -->
<button @click.once="doThis"></button>
<!-- 对象语法 -->
<button v-on="{ mousedown: doThis, mouseup: doThat }"></button>
事件修饰符
| 修饰符 | 说明 |
|---|---|
.stop | 调用 event.stopPropagation() |
.prevent | 调用 event.preventDefault() |
.capture | 使用捕获模式添加事件监听器 |
.self | 只有事件从元素本身触发时才调用处理函数 |
.{keyAlias} | 只在特定按键按下时触发 |
.once | 最多触发一次 |
.left | 只在左键鼠标事件触发 |
.right | 只在右键鼠标事件触发 |
.middle | 只在中键鼠标事件触发 |
.passive | 以 { passive: true } 模式附加 DOM 事件 |
v-bind(简写 :)
动态绑定一个或多个属性,或组件 prop 到表达式:
<!-- 绑定属性 -->
<img v-bind:src="imageSrc" />
<!-- 动态属性名 -->
<button v-bind:[key]="value"></button>
<!-- 简写 -->
<img :src="imageSrc" />
<!-- 同名简写(3.4+),展开为 :src="src" -->
<img :src />
<!-- 简写动态属性名 -->
<button :[key]="value"></button>
<!-- 内联字符串拼接 -->
<img :src="'/path/to/images/' + fileName" />
<!-- class 绑定 -->
<div :class="{ red: isRed }"></div>
<div :class="[classA, classB]"></div>
<div :class="[classA, { classB: isB, classC: isC }]"></div>
<!-- style 绑定 -->
<div :style="{ fontSize: size + 'px' }"></div>
<div :style="[styleObjectA, styleObjectB]"></div>
<!-- 绑定对象 -->
<div v-bind="{ id: someProp, 'other-attr': otherProp }"></div>
修饰符
| 修饰符 | 说明 |
|---|---|
.camel | 将短横线属性名转换为驼峰 |
.prop | 强制绑定为 DOM 属性(3.2+) |
.attr | 强制绑定为 DOM attribute(3.2+) |
.prop 修饰符有专门的简写 .:
<div :someProperty.prop="someObject"></div>
<!-- 等同于 -->
<div .someProperty="someObject"></div>
v-model
在表单输入元素或组件上创建双向绑定:
<!-- 文本输入 -->
<input v-model="message" />
<!-- 多行文本 -->
<textarea v-model="message"></textarea>
<!-- 复选框 -->
<input type="checkbox" v-model="checked" />
<!-- 多个复选框绑定到数组 -->
<input type="checkbox" value="apple" v-model="fruits" />
<input type="checkbox" value="banana" v-model="fruits" />
<!-- 单选按钮 -->
<input type="radio" value="one" v-model="picked" />
<!-- 下拉选择 -->
<select v-model="selected">
<option>A</option>
<option>B</option>
</select>
修饰符
| 修饰符 | 说明 |
|---|---|
.lazy | 在 change 事件后同步(而非 input 事件) |
.number | 自动将输入值转换为数字 |
.trim | 自动去除首尾空白 |
v-slot(简写 #)
定义具名插槽或接收 prop 的作用域插槽:
<!-- 具名插槽 -->
<BaseLayout>
<template v-slot:header>
页眉内容
</template>
<template v-slot:default>
默认插槽内容
</template>
<template v-slot:footer>
页脚内容
</template>
</BaseLayout>
<!-- 简写 -->
<BaseLayout>
<template #header>页眉</template>
<template #default>内容</template>
<template #footer>页脚</template>
</BaseLayout>
<!-- 接收 props 的作用域插槽 -->
<InfiniteScroll>
<template v-slot:item="slotProps">
<div>{{ slotProps.item.text }}</div>
</template>
</InfiniteScroll>
<!-- 解构 props -->
<Mouse v-slot="{ x, y }">
鼠标位置: {{ x }}, {{ y }}
</Mouse>
v-pre
跳过该元素及其所有子元素的编译:
<span v-pre>{{ 这里的内容不会被编译 }}</span>
在带有 v-pre 的元素内部,所有 Vue 模板语法都会保留并按原样渲染。最常见的用例是显示原始双大括号标签。
v-once
只渲染元素和组件一次,跳过以后的更新:
<!-- 单个元素 -->
<span v-once>这永远不会改变: {{ msg }}</span>
<!-- 有子元素 -->
<div v-once>
<h1>评论</h1>
<p>{{ msg }}</p>
</div>
<!-- 组件 -->
<MyComponent v-once :comment="msg" />
<!-- v-for 指令 -->
<ul>
<li v-for="i in list" v-once>{{ i }}</li>
</ul>
在随后的重新渲染中,该元素及其所有子节点将被视为静态内容并跳过。这可以用于优化更新性能。
从 3.2 开始,你还可以使用带有失效条件的 v-memo 来记忆模板的部分内容。
v-memo(Vue 3.2+)
记忆模板的子树:
<div v-memo="[valueA, valueB]">
...
</div>
当组件重新渲染时,如果 valueA 和 valueB 都与上次渲染时相同,则对该 <div> 及其所有子节点的更新将被跳过。实际上,甚至连虚拟 DOM VNode 的创建也会被跳过,因为可以重用记忆的子树副本。
<!-- 与 v-for 配合使用 -->
<div v-for="item in list" :key="item.id" v-memo="[item.id === selected]">
<p>ID: {{ item.id }} - 已选中: {{ item.id === selected }}</p>
</div>
v-cloak
用于隐藏尚未编译的模板:
<div v-cloak>
{{ message }}
</div>
[v-cloak] {
display: none;
}
v-cloak 会保留在元素上,直到关联的组件实例被挂载。结合 CSS 规则如 [v-cloak] { display: none },它可用于隐藏未编译的模板,直到组件准备就绪。
这个指令只在无构建步骤的设置中需要。
小结
本章我们详细学习了 Vue 模板语法的完整内容:
- 文本插值:使用
{{ }}显示数据,v-once执行一次性绑定 - 原始 HTML:使用
v-html插入 HTML 内容(注意安全) - 属性绑定:使用
v-bind或:绑定 HTML 属性 - JavaScript 表达式:在绑定中使用完整的 JavaScript 表达式
- 指令参数:使用
:指定指令参数 - 动态参数:使用
[expression]动态指定参数名 - 修饰符:使用
.添加特殊行为 - 内置指令:完整的指令列表和用法
练习
- 创建一个使用动态属性名的组件,实现动态切换绑定的属性
- 使用
v-bind同时绑定多个属性到一个元素 - 创建一个使用事件修饰符的表单,演示
.stop、.prevent的效果
准备好进入下一章,学习条件渲染的详细内容了吗?