Appearance
模板语法
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
更新元素的 innerHTML:v-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) 的值可以是:
- 内联事件处理器:事件被触发时执行的内联 JavaScript 语句 (与
onclick
类似)。 - 方法事件处理器:一个指向组件上定义的方法的属性名或是路径。
内联事件处理器
内联事件处理器通常用于简单场景,例如:
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>
事件修饰符
事件修饰符是用 .
表示的指令后缀,可以简化事件的特殊操作。例如:
- event.preventDefault() 可以被简化为
.prevent
- 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 表达式可以被使用在如下场景上:
- 在文本插值中 (双大括号)
- 在任何 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-value
和 false-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-if
和 v-show
两种条件渲染指令。其中 v-if 又可以配合 v-else
和 v-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-if
比 v-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 值的注意事项
key
绑定的值期望是一个基础类型的值,例如字符串或 number 类型。不要用对象作为v-for
的 key。key
的值必须具有唯一性,不能重复,建议使用数据项的id
值作为 key 的值。- 不要使用循环项的索引当做 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
在“组件高级”相关的章节中会讲解此指令的具体用法。