跳到主要内容

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>
注意

绑定表达式中调用的函数将在每次组件更新时被调用,因此它们不应该有任何副作用,比如改变数据或触发异步操作。

受限的全局访问

模板表达式被沙箱化,只能访问有限的全局列表。该列表暴露了常用的内置全局变量,如 MathDate

没有显式包含在列表中的全局变量,例如用户在 window 上附加的属性,将无法在模板表达式中访问。你可以通过将其添加到 app.config.globalProperties 来显式地为所有 Vue 表达式定义其他全局变量。

指令

指令是带有 v- 前缀的特殊属性。Vue 提供了许多内置指令,包括我们上面介绍的 v-htmlv-bind

指令属性值预期是单个 JavaScript 表达式v-forv-onv-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>

参数是事件名称,这里是 clickv-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-onv-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-ifv-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>

当组件重新渲染时,如果 valueAvalueB 都与上次渲染时相同,则对该 <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 模板语法的完整内容:

  1. 文本插值:使用 {{ }} 显示数据,v-once 执行一次性绑定
  2. 原始 HTML:使用 v-html 插入 HTML 内容(注意安全)
  3. 属性绑定:使用 v-bind: 绑定 HTML 属性
  4. JavaScript 表达式:在绑定中使用完整的 JavaScript 表达式
  5. 指令参数:使用 : 指定指令参数
  6. 动态参数:使用 [expression] 动态指定参数名
  7. 修饰符:使用 . 添加特殊行为
  8. 内置指令:完整的指令列表和用法

练习

  1. 创建一个使用动态属性名的组件,实现动态切换绑定的属性
  2. 使用 v-bind 同时绑定多个属性到一个元素
  3. 创建一个使用事件修饰符的表单,演示 .stop.prevent 的效果

准备好进入下一章,学习条件渲染的详细内容了吗?