Skip to content

模板语法

vue 拓展了 HTML 的语法,提供了诸如 v-text、文本插值、v-bind、v-for、v-on 等 vue 专属语法(指令),用来帮助程序员快速渲染模板页面的数据

内容渲染指令

v-text*

更新元素的文本内容v-text 通过设置元素的 textContent 属性来工作,因此它将覆盖元素中所有现有的内容。

例如:

html
<span v-text="msg"></span>

如果只想更新特定区域的文本内容,而非覆盖元素中所有的文本内容,推荐使用 文本插值。

文本插值

文本插值可以实现元素中文本内容的部分更新。它使用的是“Mustache”语法 (即双大括号):

html
<span>年龄是: {{ age }}</span>

双大括号标签会被替换为相应组件实例中 age 属性的值。同时每次 age 属性更改时它也会同步更新。

v-html

更新元素的 innerHTMLv-html 的内容直接作为普通 HTML 渲染到页面上。

例如:

html
<div v-html="htmlStr"></div>

v-cloak*

用于隐藏尚未完成编译的 DOM 模板。该指令只在没有构建步骤的环境下需要使用。

当使用直接在 DOM 中书写的模板时,可能会出现一种叫做“未编译模板闪现”的情况:用户可能先看到的是还没编译完成的双大括号标签,直到挂载的组件将它们替换为实际渲染的内容。

v-cloak 会保留在所绑定的元素上,直到相关组件实例被挂载后才移除。配合像 [v-cloak] { display: none } 这样的 CSS 规则,它可以在组件编译完毕前隐藏原始模板。

css
[v-cloak] {
  display: none;
}
html
<div id="app" v-cloak>
  <h3>count 的值是:{{count}}</h3>
</div>

注意

此指令今后几乎不会用到,因为实际项目开发中都会基于构建工具进行项目开发,不存在页面闪烁的情况。

v-pre*

跳过该元素及其所有子元素的编译。

元素内具有 v-pre,所有 Vue 模板语法都会被保留并按原样渲染。最常见的用例就是显示原始双大括号标签及内容。

html
<span v-pre>{{ this will not be compiled }}</span>

事件绑定指令

我们可以使用 v-on 指令 (简写为 @) 来监听 DOM 事件,并在事件触发时执行对应的 JavaScript。用法:v-on:click="handler"@click="handler"

事件处理器 (handler) 的值可以是:

  1. 内联事件处理器:事件被触发时执行的内联 JavaScript 语句 (与 onclick 类似)。
  2. 方法事件处理器:一个指向组件上定义的方法的属性名或是路径。

内联事件处理器

内联事件处理器通常用于简单场景,例如:

js
const count = ref(0)
html
<button @click="count++">Add 1</button>
<p>Count is: {{ count }}</p>

注意

如果事件的处理逻辑只有一行代码,可以简写为内联事件处理器,而不必单独定义对应的方法事件处理器。

方法事件处理器

随着事件处理器的逻辑变得愈发复杂,内联代码方式变得不够灵活。因此 v-on 也可以接受一个方法名或对某个方法的调用

方法名

在绑定方法事件处理器时,如果不需要传参,可以直接绑定方法名:

html
<p>count 的值:{{ count }}</p>
<button @click="add">+1</button>
js
// 按钮的方法事件处理函数
const add = (e) => {
  count.value++
  e.target.classList.toggle('skin')
}

配套的 CSS 样式:

css
.skin {
  --w: #fff;
  background: linear-gradient(45deg, green, cyan);
  color: var(--w);
  border: 2px solid var(--w);
  padding: 5px 15px;
  border-radius: 6px;
}

注意

直接绑定方法名,可以接收到事件参数对象 e 并使用它。

方法的调用

在绑定方法事件处理器时,如果需要传参,可以绑定方法的调用:

html
<p>count 的值:{{ count }}</p>
<button @click="add(1, $event)">+1</button>
<button @click="add(2, $event)">+2</button>
js
// 按钮的方法事件处理函数
const add = (step, e) => {
  count.value += step
  e.target.classList.toggle('skin')
}

注意

绑定方法的调用并传参后,会覆盖事件参数对象 e。如有需要可以手动传递名为 $event 的特殊变量、或者使用内联的箭头函数

html
<!-- 使用内联箭头函数 -->
<button @click="(event) => warn('Form cannot be submitted yet.', event)">Submit</button>

事件修饰符

事件修饰符是用 . 表示的指令后缀,可以简化事件的特殊操作。例如:

  1. event.preventDefault() 可以被简化为 .prevent
  2. event.stopPropagation() 可以被简化为 .stop

.prevent 阻止默认行为

html
<!-- 指定事件修饰符,同时绑定方法事件处理器 -->
<a href="https://escook.cn" @click.prevent="show">我的博客</a>
<!-- 仅指定事件修饰符 -->
<a href="https://escook.cn" @click.prevent>我的博客</a>

.stop 阻止事件冒泡

html
<div class="outer" @click="console.log('outer')">
  <div class="inner" @click="console.log('inner')">
    <button @click.stop="console.log('btn')">click me</button>
  </div>
</div>

配套的 CSS 样式:

css
.outer {
  height: 200px;
  width: 200px;
  background-color: lightblue;
  display: flex;
  justify-content: center;
  align-items: center;
}

.inner {
  height: 100px;
  width: 100px;
  background-color: orange;
  display: flex;
  justify-content: center;
  align-items: center;
}

同时,修饰符可以链式书写,例如为 a 链接的 @click 同时应用 .stop.prevent 修饰符:

html
<div class="outer" @click="console.log('outer')">
  <div class="inner" @click="console.log('inner')">
    <a href="https://escook.cn" @click.stop.prevent="console.log('link')">my blog</a>
  </div>
</div>

.self 限定事件触发源

html
<div class="outer" @click="console.log('outer')">
  <!-- 仅当 event.target 是元素本身时才会触发事件处理器 -->
  <div class="inner" @click.self="console.log('inner')">
    <button @click="console.log('btn')">click me</button>
  </div>
</div>

注意

.self 修饰符不会阻止事件冒泡,而是判断事件触发源是否为当前元素。

.once 限定事件触发次数

html
<!-- 点击事件最多被触发一次 -->
<div class="outer" @click.once="console.log('outer')">
  <div class="inner" @click.self="console.log('inner')">
    <button @click="console.log('btn')">click me</button>
  </div>
</div>

按键修饰符

在监听键盘事件时,我们经常需要检查特定的按键。Vue 允许在 v-on@ 监听按键事件时添加按键修饰符。

html
<!-- 按下 esc 时清空文本框 -->
<input type="text" @keyup.esc="keyupHandler" />
js
const keyupHandler = (e) => {
  e.currentTarget.value = ''
}

按键修饰符和事件修饰符的区别

按键修饰符只能配合键盘相关的事件一起使用,例如 keyup/keydown/keypress。但是事件修饰符可以配合任何事件使用。

按键别名

Vue 为一些常用的按键提供了别名:

  • .enter
  • .tab
  • .delete (捕获“Delete”和“Backspace”两个按键)
  • .esc
  • .space
  • .up
  • .down
  • .left
  • .right

除此之外,我们可以直接使用 KeyboardEvent.key 暴露的按键名称作为修饰符,但需要转为 kebab-case 形式。例如:

html
<!-- 按下 CapsLock 时把文本框内容转为首字母大写的形式 -->
<input type="text" @keyup.caps-lock="capitalizeHandler" />
js
const capitalizeHandler = (e) => {
  const value = e.currentTarget.value
  if (value.length === 0) return
  e.currentTarget.value = value[0].toUpperCase() + value.slice(1)
}

系统按键修饰符

我们还可以使用以下系统按键修饰符来触发鼠标键盘事件监听器,只有当按键被按下时才会触发。

  • .ctrl
  • .alt
  • .shift
  • .meta

例如,监听文本框的 Alt + U 和 Alt + L 快捷键,实现文本的大小写转换:

html
<input type="text" @keyup.alt.u="upperHandler" @keyup.alt.l="lowerHandler" />
js
const upperHandler = (e) => {
  // 把文本转为大写
  e.currentTarget.value = e.currentTarget.value.toUpperCase()
}

const lowerHandler = (e) => {
  // 把文本转为小写
  e.currentTarget.value = e.currentTarget.value.toLowerCase()
}

另外,.exact 修饰符允许精确控制触发事件所需的系统修饰符的组合:

html
<!-- 仅当按下了 Alt + U 时才触发 upperHandler 处理函数 -->
<!-- 仅当按下了 Alt + L 时才触发 lowerHandler 处理函数 -->
<input type="text" @keyup.alt.u.exact="upperHandler" @keyup.alt.l.exact="lowerHandler" />

鼠标按键修饰符

  • .left
  • .right
  • .middle

这些修饰符将处理程序限定为由特定鼠标按键触发的事件。示例代码如下:

html
<img
  src="./image/1.jpg"
  alt=""
  width="200px"
  @click.left="magnifyHandler"
  @click.right.prevent="shrinkHandler"
  @click.middle="rotateHandler"
  @click.ctrl.middle.exact="rotateBackHandler"
/>
js
// 放大图片
const magnifyHandler = (e) => {
  e.currentTarget.style.width = '400px'
}
// 缩小图片
const shrinkHandler = (e) => {
  e.currentTarget.style.width = '200px'
}
// 图片旋转45度
const rotateHandler = (e) => {
  e.currentTarget.style.transform = 'rotateZ(45deg)'
}
// 图片旋转0度(恢复旋转角度)
const rotateBackHandler = (e) => {
  e.currentTarget.style.transform = 'rotateZ(0deg)'
}

属性绑定指令

文本插值只能用于元素的内容区域,如果要为元素的属性绑定动态的值,则需要用到 v-bind 指令(简写为英文的 :)。示例代码如下:

html
<!-- 动态绑定 img 元素的 src 属性的值和 width 属性的值 -->
<img v-bind:src="imgURL" alt="" :width="width" />
<br />
<!-- 点击按钮,更新图片的 URL 值 -->
<button @click="imgURL = './image/2.jpg'">change image</button>
<!-- 点击按钮,图片宽度 +10px -->
<button @click="width+=10">+10</button>
js
const imgURL = ref('./image/1.jpg')
const width = ref(100)

同名简写

如果 attribute 的名称与绑定的 JavaScript 值的名称相同,那么可以进一步简化语法,省略 attribute 值:

html
<img v-bind:src="imgURL" alt="" :width />
<!-- 等价于下面的写法 -->
<img v-bind:src="imgURL" alt="" v-bind:width />

注意

同名简写的语法是 vue 3.4 新增的功能。

动态绑定多个值

我们可以把需要动态绑定的多个属性和值封装为一个 JavaScript 对象,然后通过不带参数的 v-bind 为元素一次性绑定多个属性:

js
const attrObj = {
  title: '这是一个文本框',
  id: 'ipt',
  value: 'abc',
  placeholder: '请输入账号'
}
html
<!-- 挨个展开每个属性,太麻烦了 -->
<input :title="attrObj.title" :id="attrObj.id" :value="attrObj.value" :placeholder="attrObj.placeholder" />
html
<!-- 把对象上的属性,一次性绑定到元素上 -->
<input type="text" v-bind="attrObj" />
<!-- 最终生成的 HTML 标签结构如下: -->
<input type="text" title="这是一个文本框" id="ipt" placeholder="请输入账号" value="abc" />

使用 JavaScript 表达式

什么是表达式

表达式指的是一段能够被求值的 JavaScript 代码。一个简单的判断方法是是否可以合法地写在 return 的后面。例如下面的 JavaScript 代码都是表达式:

js
'abc'
true
123
num + 2
ok ? 'YES' : 'NO'
message.split('').reverse().join('')`list-${id}`

请注意:语句不是表达式,例如下面的代码都是语句,并非表达式:

js
const name = 'zs'
if (ok) {
  return message
}

表达式的使用场景

在 Vue 模板内,JavaScript 表达式可以被使用在如下场景上:

  1. 在文本插值中 (双大括号)
  2. 在任何 Vue 指令 (以 v- 开头的特殊 attribute) attribute 的值中

双向绑定指令

v-model 是 vue 中的双向数据绑定指令,经常配合表单元素使用。可以自动的把数据源更新到表单元素中,也能够自动把表单数据的变化更新到数据源。

例如 input 元素配合 v-model 指令实现数据的双向绑定:

html
<div id="app">
  <p>文本框的值:{{ content }}</p>
  <!-- 修改表单元素的值,会把数据更新到“数据源”中 -->
  <input type="text" v-model="content" />

  <!-- 点击按钮,直接修改"数据源",表单数据会自动被更新 -->
  <button @click="content = 'hello world.'">为 content 赋值</button>
</div>
js
const content = ref('')

多行文本框

v-model 与 textarea 配合使用时,与 input 的用法类似:

html
<input type="text" v-model="content" /> <textarea v-model="article"></textarea>

复选框

单一复选框

单一复选框,通过 v-model 绑定布尔类型的值:

html
<input type="checkbox" id="cb1" v-model="state" /> <label for="cb1">{{state}}</label>

数据源如下:

js
const state = ref(false) // 初值为 false

单一复选框的值绑定

默认情况下,单一复选框绑定的是布尔值 true 或 false。

vue 提供了 true-valuefalse-value 两个属性,允许我们自定义选中/未选中时候的值:

html
<!-- true-value="yes" 表示选中状态时,复选框的值为字符串 yes -->
<!-- false-value="no" 表示非选中状态时,复选框的值为字符串 no -->
<input type="checkbox" id="cb2" v-model="state2" true-value="yes" false-value="no" />
<label for="cb2">{{state2}}</label>

数据源如下:

js
const state2 = ref('no')

多个复选框

vue 允许将多个复选框绑定到同一个数组中:

html
<p>多个复选框:{{hobby}}</p>
<label> <input type="checkbox" v-model="hobby" value="吃饭" />吃饭 </label>
<label> <input type="checkbox" v-model="hobby" value="睡觉" />睡觉 </label>
<label> <input type="checkbox" v-model="hobby" value="打豆豆" />打豆豆 </label>

对应的数据源为:

js
const hobby = ref([])

单选按钮

多个单选按钮绑定到同一个数据源,可以实现互斥的单选操作。被选中的值需要通过 value 属性指定:

html
<p>性别:{{gender}}</p>
<label> <input type="radio" v-model="gender" value="男" />男 </label>
<label> <input type="radio" v-model="gender" value="女" />女 </label>
<label> <input type="radio" v-model="gender" value="保密" />保密 </label>

对应的数据源为:

js
const gender = ref('男')

选择器

单选

html
<p>学位:{{degree}}</p>
<select v-model="degree">
  <option value="" disabled>请选择</option>
  <option>博士</option>
  <option>硕士</option>
  <option>学士</option>
</select>

对应的数据源为:

js
const degree = ref('')

多选

被选中的多个值绑定到一个数组中:

html
<p>喜欢的水果:{{fruits}}</p>
<select v-model="fruits" multiple>
  <option>苹果</option>
  <option>榴莲</option>
  <option>释迦果</option>
</select>

对应的数据源为:

js
const fruits = ref([])

值绑定

默认情况下,<option> 的内容会被当做选中后的值。如果为 <option> 提供了 value 属性,则 value 会被当做选中后的值:

html
<p>喜欢的水果:{{fruits}}</p>
<select v-model="fruits" multiple>
  <option value="apple">苹果</option>
  <option value="durian">榴莲</option>
  <option value="custard apple">释迦果</option>
</select>

v-model 的专属修饰符

.lazy

默认情况下,v-model 会在每次 input 事件后更新数据 (IME 拼字阶段的状态例外)。你可以添加 lazy 修饰符来改为在每次 change 事件后更新数据:

html
<!-- 在 "change" 事件后同步更新而不是 "input" -->
<input v-model.lazy="msg" />

.number

如果你想让用户输入自动转换为数字,你可以在 v-model 后添加 .number 修饰符来管理输入:

html
<input v-model.number="age" />

如果该值无法被 parseFloat() 处理,那么将返回原始值。

number 修饰符会在输入框有 type="number" 时自动启用。

.trim

如果你想要默认自动去除用户输入内容中两端的空格,你可以在 v-model 后添加 .trim 修饰符:

html
<input v-model.trim="msg" />

条件渲染指令

如果想按条件渲染页面上的元素,就需要用到 vue 提供的条件渲染指令了。vue 提供了 v-ifv-show 两种条件渲染指令。其中 v-if 又可以配合 v-elsev-else-if 实现多条件分支的渲染控制。

v-if

被 v-if 指令控制的元素会在条件为 true 时被渲染。若条件为 false,则被控制的元素不会被渲染到页面上:

html
<div v-if="flag">这是被 v-if 控制的 div</div>
<button @click="flag = !flag">toggle flag</button>

对应的数据源为:

js
const flag = ref(false)

v-else-if 和 v-else

v-if 指令可以单独使用。也可以配合可选的 v-else-if 和 v-else 指令一起使用,用来匹配不同的分支条件。类似于 JavaScript 中的 if...else if...else 的用法。

html
<span>打分:</span>
<select v-model="type">
  <option>A</option>
  <option>B</option>
  <option>C</option>
  <option>D</option>
</select>

<p>评分类型:</p>
<p v-if="type === 'A'">优秀</p>
<p v-else-if="type === 'B'">良好</p>
<p v-else-if="type === 'C'">一般</p>
<p v-else>重修</p>

对应的数据源为:

js
const type = ref('A')

template 上的 v-if

如果 v-if 要同时控制多个元素的显示和隐藏,可以将多个元素包裹在虚拟的 <template> 容器中,好处是避免渲染不必要的容器元素。因为 <template> 不会被渲染到 HTML 中,只起到一个虚拟的包裹容器的作用。

html
<button @click="show = !show">toggle show</button>
<!-- 请注意:下面用到了 template 包裹多个元素,而非使用 div 进行包裹。 -->
<!-- 能够减少一层不必要的 div 嵌套 -->
<template v-if="show">
  <p>锄禾日当午,</p>
  <p>汗滴禾下土。</p>
  <p>谁知盘中餐,</p>
  <p>粒粒皆辛苦。</p>
</template>

对应的数据源为:

js
const show = ref(false)

v-show

和 v-if 指令功能类似的,v-show 指令也能控制元素的显示和隐藏。例如:

html
<!-- 如果 ok 的值为 true 则显示被控制的元素;否则隐藏被控制的元素。 -->
<h1 v-show="ok">被 v-show 控制的元素</h1>

v-if 与 v-show 的对比

v-if 是“真实的”按条件渲染,因为它确保了在切换时,条件区块内的事件监听器和子组件都会被销毁与重建。

v-if 也是惰性的:如果在初次渲染时条件值为 false,则不会做任何事。条件区块只有当条件首次变为 true 时才被渲染。

相比之下,v-show 简单许多,元素无论初始条件如何,始终会被渲染,只有 CSS display 属性会被切换。

TIP

总的来说,v-if 有更高的切换开销,而 v-show 有更高的初始渲染开销。 因此,如果需要频繁切换,则使用 v-show 较好;如果在运行时绑定条件很少改变,则 v-if 会更合适。

列表渲染指令

基本语法

v-for 是列表渲染指令,可以把数组元素循环渲染为列表结构

v-for 指令的值需要使用 item in items 形式的特殊语法,其中 items 是源数据的数组,而 item 是迭代项的别名

js
const items = ref([{ message: 'Foo' }, { message: 'Bar' }])
html
<li v-for="item in items">{{ item.message }}</li>

v-for 也支持使用可选的第二个参数表示当前项的位置索引:

html
<li v-for="(item, index) in items">{{ index }} - {{ item.message }}</li>

template 上的 v-for

虚拟容器 <template> 也可以配合 v-for 指令使用,从而防止生成多余的容器元素:

js
const list = ref([{ name: 'zs' }, { name: 'ls' }, { name: 'xh' }])
html
<div id="app">
  <ul>
    <!-- 循环生成 li 列表 -->
    <template v-for="(item, i) in list">
      <li>{{item.name}}</li>
      <li class="divider" v-if="i !== list.length - 1">------------</li>
    </template>
  </ul>
</div>

配套的 CSS 样式如下:

css
.divider {
  list-style: none;
}

v-for 和 v-if

当 v-for 和 v-if 同时存在于一个节点上时,v-ifv-for 的优先级更高。这意味着 v-if 的条件将无法访问到 v-for 作用域内定义的变量别名:

html
<!--
 这会抛出一个错误,因为属性 todo 此时
 没有在该实例上定义
-->
<li v-for="todo in todos" v-if="!todo.isComplete">{{ todo.name }}</li>

在外先包装一层 <template> 再在其上使用 v-for 可以解决这个问题 (这也更加明显易读):

html
<template v-for="todo in todos">
  <li v-if="!todo.isComplete">{{ todo.name }}</li>
</template>

再次强调

由于 v-if 的优先级高于 v-for,因此 v-if 中如果用到了 v-for 中的变量,会发生变量未定义的情况。因为此时 v-for 指令还没有被 vue 解析。

通过 key 管理状态

vue 官方推荐 v-for 指令搭配 key 值一起使用,从而防止 v-for 列表渲染异常的问题。

就地更新策略

在没有使用 key 的情况下,vue 将使用一种最小化元素移动的算法,并尽可能地就地复用/更新相同类型的元素。

js
const app = createApp({
  setup() {
    const list = ref([{ name: '张三' }, { name: '李四' }, { name: '小红' }])

    const name = ref('')
    const add = () => {
      list.value.unshift({ name: name.value })
    }

    return { list, name, add }
  }
})
html
<div id="app">
  <input type="text" v-model="name" />
  <button @click="add">添加</button>
  <hr />

  <ul>
    <!-- 循环生成 li 列表 -->
    <template v-for="(item, i) in list">
      <li>
        <span>{{item.name}}</span>
        <input type="text" />
      </li>
    </template>
  </ul>
</div>

对比策略

逐行比较,判断元素类型是否发生变化。

  • 如果更新前后元素类型没变,则就地复用元素,只更新元素的内容和属性
  • 如果更新前后元素类型变化了,则移除旧元素,创建新元素。

key 值更新策略

如果传了 key,则将根据 key 的变化顺序重新排列元素,并且将始终移除/销毁 key 已经不存在的元素。

js
const app = createApp({
  setup() {
    const list = ref([
      { name: '张三', id: 1 },
      { name: '李四', id: 2 },
      { name: '小红', id: 3 }
    ])

    const name = ref('')
    const nextId = ref(4) // 下一个可用的 id 值
    const add = () => {
      list.value.unshift({ name: name.value, id: nextId.value++ })
    }

    return { list, name, add }
  }
})
html
<div id="app">
  <input type="text" v-model="name" />
  <button @click="add">添加</button>
  <hr />

  <ul>
    <!-- 循环生成 li 列表 -->
    <template v-for="(item, i) in list" :key="item.id">
      <li>
        <span>{{item.name}}</span>
        <input type="text" />
      </li>
    </template>
  </ul>
</div>

对比策略

根据更新前后 key 值的变化顺序来重排元素,从而实现同类型元素内容属性的完全复用。

key 值的注意事项

  1. key 绑定的值期望是一个基础类型的值,例如字符串或 number 类型。不要用对象作为 v-for 的 key。
  2. key 的值必须具有唯一性,不能重复,建议使用数据项的 id 值作为 key 的值。
  3. 不要使用循环项的索引当做 key 值,因为索引不具有唯一性。

其它指令

v-once

仅渲染元素和组件一次,并跳过之后的更新。

在随后的重新渲染中,元素/组件及其所有子项将被当作静态内容并跳过渲染。这可以用来优化更新时的性能。

html
<!-- h3 的内容会更新渲染 -->
<h3>count 的值是:{{count}}</h3>
<!-- h4 的内容不会被更新渲染 -->
<h4 v-once>count 的值是:{{count}}</h4>
<!-- 点击按钮 count 值自增+1 -->
<button @click="count++">+1</button>

v-slot

用于声明具名插槽或是期望接收 props 的作用域插槽。

TIP

在“组件高级”相关的章节中会讲解此指令的具体用法。

天不生夫子,万古长如夜