计算属性
为什么要用计算属性
模板中的表达式虽然方便,但也只能用来做简单的操作。如果在模板中写太多逻辑,会让模板变得臃肿,难以维护。比如说,我们想根据 author
是否已有一些书籍来展示不同的信息:
<script setup>
const author = reactive({
name: 'John Doe',
books: [
'Vue 2 - Advanced Guide',
'Vue 3 - Basic Guide',
'Vue 4 - The Mystery'
]
})
</script>
<template>
<p>Has published books:</p>
<span>{{ author.books.length > 0 ? 'Yes' : 'No' }}</span>
<template>
这里的模板看起来有些复杂,必须认真看好一会儿才能明白它的计算依赖于 author.books
。更重要的是,如果在模板中需要不止一次这样的计算,我们可不想将这样的代码在模板里重复好多遍。因此需要使用计算属性来描述依赖响应式状态的复杂逻辑:
<script setup>
import { reactive, computed } from 'vue'
const author = reactive({
name: 'John Doe',
books: [
'Vue 2 - Advanced Guide',
'Vue 3 - Basic Guide',
'Vue 4 - The Mystery'
]
})
// 一个计算属性 ref
const publishedBooksMessage = computed(() => {
return author.books.length > 0 ? 'Yes' : 'No'
})
</script>
<template>
<p>Has published books:</p>
<span>{{ publishedBooksMessage }}</span>
</template>
我们在这里定义了一个计算属性 publishedBooksMessage
。computed()
方法期望接收一个 getter
函数,返回值为一个计算属性 ref
。和其他一般的 ref
类似,你可以通过 publishedBooksMessage.value
访问计算结果。计算属性 ref
也会在模板中自动解包,因此在模板表达式中引用时无需添加 .value
。
Vue 的计算属性会自动追踪响应式依赖。它会检测到 publishedBooksMessage
依赖于 author.books
,所以当 author.books
改变时,任何依赖于 publishedBooksMessage
的绑定都会同时更新。
计算属性缓存 vs 方法
若我们将同样的函数定义为一个方法而不是计算属性,两种方式在结果上确实是完全相同的,然而,不同之处在于计算属性值会基于其响应式依赖被缓存。一个计算属性仅会在其响应式依赖更新时才重新计算。这意味着只要 author.books
不改变,无论多少次访问 publishedBooksMessage
都会立即返回先前的计算结果,而不用重复执行 getter
函数。
这也解释了为什么下面的计算属性永远不会更新,因为 Date.now()
并不是一个响应式依赖,相比之下,方法调用总是会在重渲染发生时再次执行函数。
const now = computed(() => Date.now())
可写计算属性
计算属性默认是只读的。当你尝试修改一个计算属性时,你会收到一个运行时警告。只在某些特殊场景中你可能才需要用到“可写”的属性,你可以通过同时提供 getter
和 setter
来创建:
<script setup>
import { ref, computed } from 'vue'
const firstName = ref('John')
const lastName = ref('Doe')
const fullName = computed({
// getter
get() {
return firstName.value + ' ' + lastName.value
},
// setter
set(newValue) {
// 注意:我们这里使用的是解构赋值语法
[firstName.value, lastName.value] = newValue.split(' ')
}
})
</script>
现在当你再运行 fullName.value = 'John Doe'
时,setter
会被调用而 firstName
和 lastName
会随之更新。
正确使用
Getter 不应有副作用
计算属性的 getter
应只做计算而没有任何其他的副作用,这一点非常重要,请务必牢记。举例来说,不要在 getter
中做异步请求或者更改 DOM
!一个计算属性的声明中描述的是如何根据其他值派生一个值。因此 getter
的职责应该仅为计算和返回该值。在之后的指引中我们会讨论如何使用监听器根据其他响应式状态的变更来创建副作用。
避免直接修改计算属性值
从计算属性返回的值是派生状态。可以把它看作是一个“临时快照”,每当源状态发生变化时,就会创建一个新的快照。更改快照是没有意义的,因此计算属性的返回值应该被视为只读的,并且永远不应该被更改——应该更新它所依赖的源状态以触发新的计算。
类与样式绑定
数据绑定的一个常见需求场景是操纵元素的 CSS class
列表和内联样式。因为 class
和 style
都是 attribute
,我们可以和其他 attribute
一样使用 v-bind
将它们和动态的字符串绑定。但是,在处理比较复杂的绑定时,通过拼接生成字符串是麻烦且易出错的。因此,Vue 专门为 class
和 style
的 v-bind
用法提供了特殊的功能增强。除了字符串外,表达式的值也可以是对象或数组。
绑定 class
绑定多个 class
<script setup>
const isActive = ref(true)
const hasError = ref(false)
</script>
<template>
<div class="static" :class="{ active: isActive, 'text-danger': hasError }"></div>
</template>
直接绑定一个 class 对象
<script setup>
const classObject = reactive({
active: true,
'text-danger': false
})
</script>
<template>
<div :class="classObject"></div>
</template>
绑定 class 数组
<script setup>
const activeClass = ref('active')
const errorClass = ref('text-danger')
</script>
<template>
<div :class="[isActive ? activeClass : '', errorClass]"></div>
<div :class="[{ active: isActive }, errorClass]"></div>
</template>
子组件 class 传递
<!-- 子组件模板 -->
<p class="foo bar">Hi!</p>
<!-- 在使用组件时 -->
<MyComponent class="baz boo" />
<!-- 子组件渲染结果-->
<p class="foo bar baz boo">Hi!</p>
如果你的组件有多个根元素,你将需要指定哪个根元素来接收这个 class
。你可以通过组件的 $attrs
属性来实现指定:
<p :class="$attrs.class">Hi!</p>
<span>This is a child component</span>
<MyComponent class="baz" />
<!-- 子组件渲染结果-->
<p class="baz">Hi!</p>
<span>This is a child component</span>
绑定 style
常用绑定
style
与 class
的绑定方式基本相同,在此不做过多介绍:
<template>
<!-- 绑定属性-->
<div :style="{ color: activeColor, fontSize: fontSize + 'px' }"></div>
<!-- 绑定对象-->
<div :style="styleObject"></div>
<!-- 绑定数组-->
<div :style="[baseStyles, overridingStyles]"></div>
</template>
特殊情况
当你在 :style
中使用了需要浏览器特殊前缀的 CSS
属性时,Vue 会自动为他们加上相应的前缀。Vue 是在运行时检查该属性是否支持在当前浏览器中使用。如果浏览器不支持某个属性,那么将尝试加上各个浏览器特殊前缀,以找到哪一个是被支持的。
你可以对一个样式属性提供多个 (不同前缀的) 值,数组仅会渲染浏览器支持的最后一个值。在这个示例中,在支持不需要特别前缀的浏览器中都会渲染为 display: flex
。
<template>
<div :style="{ display: ['-webkit-box', '-ms-flexbox', 'flex'] }"></div>
</template>
条件渲染
批量渲染
因为 v-if
是一个指令,他必须依附于某个元素。但如果我们想要切换不止一个元素呢?在这种情况下我们可以在一个 <template>
元素上使用 v-if
,这只是一个不可见的包装器元素,最后渲染的结果并不会包含这个 <template>
元素。
<template v-if="ok">
<h1>Title</h1>
<p>Paragraph 1</p>
<p>Paragraph 2</p>
</template>
v-if 与 v-show 比较
v-if
是“真实的”按条件渲染,因为它确保了在切换时,条件区块内的事件监听器和子组件都会被销毁与重建;v-if
也是惰性的:如果在初次渲染时条件值为 false
,则不会做任何事。条件区块只有当条件首次变为 true
时才被渲染。相比之下,v-show
简单许多,元素无论初始条件如何,始终会被渲染,只有 CSS display
属性会被切换。
总的来说,v-if
有更高的切换开销,而 v-show
有更高的初始渲染开销。因此,如果需要频繁切换,则使用 v-show
较好;如果在运行时绑定条件很少改变,则 v-if
会更合适。
列表渲染
解构
在定义 v-for
的变量别名时使用解构,和解构函数参数类似:
<li v-for="{ message } in items">
{{ message }}
</li>
<!-- 有 index 索引时 -->
<li v-for="({ message }, index) in items">
{{ message }} {{ index }}
</li>
of 遍历
使用 of
作为分隔符来替代 in
,这更接近 JavaScript
的迭代器语法:
<div v-for="item of items"></div>
遍历对象
使用 v-for
来遍历一个对象的所有属性。遍历的顺序会基于对该对象调用 Object.keys()
的返回值来决定。
const myObject = reactive({
title: 'How to do lists in Vue',
author: 'Jane Doe',
publishedAt: '2016-04-10'
})
<ul>
<li v-for="value in myObject">
{{ value }}
</li>
</ul>
<li v-for="(value, key) in myObject">
{{ key }}: {{ value }}
</li>
特殊情况
同时使用 v-if 和 v-for 是不推荐的,因为这样二者的优先级不明显。
当它们同时存在于一个节点上时,v-if
比 v-for
的优先级更高。这意味着 v-if
的条件将无法访问到 v-for
作用域内定义的变量别名:
<!--
这会抛出一个错误,因为属性 todo 此时
没有在该实例上定义
-->
<li v-for="todo in todos" v-if="!todo.isComplete">
{{ todo.name }}
</li>
在外新包装一层 <template>
再在其上使用 v-for
可以解决这个问题 (这也更加明显易读):
<template v-for="todo in todos">
<li v-if="!todo.isComplete">
{{ todo.name }}
</li>
</template>
规范写法
为了给 Vue 一个提示,以便它可以跟踪每个节点的标识,从而重用和重新排序现有的元素,你需要为每个元素对应的块提供一个唯一的 key
。
<template v-for="todo in todos" :key="todo.name">
<li>{{ todo.name }}</li>
</template>
数组变化侦测
Vue 能够侦听响应式数组的变更方法,并在它们被调用时触发相关的更新。这些变更方法包括:
push()
pop()
shift()
unshift()
splice()
sort()
reverse()
其中 filter()
,concat()
和 slice()
,这些都不会更改原数组,而总是返回一个新数组。你可能认为这将导致 Vue 丢弃现有的 DOM 并重新渲染整个列表。
幸运的是,情况并非如此。Vue 实现了一些巧妙的方法来最大化对 DOM 元素的重用,因此用另一个包含部分重叠对象的数组来做替换,仍会是一种非常高效的操作。
有时,我们希望显示数组经过过滤或排序后的内容,而不实际变更或重置原始数据。在这种情况下,你可以创建返回已过滤或已排序数组的计算属性。
const numbers = ref([1, 2, 3, 4, 5])
const evenNumbers = computed(() => {
return numbers.value.filter((n) => n % 2 === 0)
})
在计算属性中使用
reverse()
和sort()
的时候务必小心!这两个方法将变更原始数组,计算函数中不应该这么做,请在调用这些方法之前创建一个原数组的副本。
事件处理
我们可以使用 v-on
指令 (简写为 @
) 来监听 DOM 事件,并在事件触发时执行对应的 JavaScript
。用法:v-on:click="handler"
或 @click="handler"
。事件处理器会自动接收原生 DOM 事件并触发执行。
访问原生 DOM 事件
有时我们需要在内联事件处理器中访问原生 DOM 事件。你可以向该处理器方法传入一个特殊的 $event
变量,或者使用内联箭头函数:
<!-- 使用特殊的 $event 变量 -->
<button @click="warn('Form cannot be submitted yet.', $event)">
Submit
</button>
<!-- 使用内联箭头函数 -->
<button @click="(event) => warn('Form cannot be submitted yet.', event)">
Submit
</button>
function warn(message, event) {
// 这里可以访问原生事件
if (event) {
event.preventDefault()
}
alert(message)
}
事件修饰符
在处理事件时调用 event.preventDefault()
或 event.stopPropagation()
是很常见的。尽管我们可以直接在方法内调用,但如果方法能更专注于数据逻辑而不用去处理 DOM 事件的细节会更好。
为解决这一问题,Vue 为 v-on
提供了事件修饰符。修饰符是用 . 表示的指令后缀,包含以下这些:
.stop
.prevent
.self
.capture
.once
.passive
<!-- 单击事件将停止传递 -->
<a @click.stop="doThis"></a>
<!-- 提交事件将不再重新加载页面 -->
<form @submit.prevent="onSubmit"></form>
<!-- 修饰语可以使用链式书写 -->
<a @click.stop.prevent="doThat"></a>
<!-- 也可以只有修饰符 -->
<form @submit.prevent></form>
<!-- 仅当 event.target 是元素本身时才会触发事件处理器 -->
<!-- 例如:事件处理器不来自子元素 -->
<div @click.self="doThat">...</div>
使用修饰符时需要注意调用顺序,因为相关代码是以相同的顺序生成的。因此使用
@click.prevent.self
会阻止元素及其子元素的所有点击事件的默认行为,而@click.self.prevent
则只会阻止对元素本身的点击事件的默认行为。
.capture
、.once
和 .passive
修饰符与原生 addEventListener
事件相对应:
<!-- 添加事件监听器时,使用 `capture` 捕获模式 -->
<!-- 例如:指向内部元素的事件,在被内部元素处理前,先被外部处理 -->
<div @click.capture="doThis">...</div>
<!-- 点击事件最多被触发一次 -->
<a @click.once="doThis"></a>
<!-- 滚动事件的默认行为 (scrolling) 将立即发生而非等待 `onScroll` 完成 -->
<!-- 以防其中包含 `event.preventDefault()` -->
<div @scroll.passive="onScroll">...</div>
.passive
修饰符一般用于触摸事件的监听器,可以用来改善移动端设备的滚屏性能。请勿同时使用.passive
和.prevent
,因为.passive
已经向浏览器表明了你不想阻止事件的默认行为。如果你这么做了,则.prevent
会被忽略,并且浏览器会抛出警告。
按键修饰符
在监听键盘事件时,我们经常需要检查特定的按键。Vue 允许在 v-on
或 @
监听按键事件时添加按键修饰符。Vue 为一些常用的按键提供了别名:
.enter
.tab
.delete
(捕获“Delete”和“Backspace”两个按键).esc
.space
.up
.down
.left
.right
<!-- 仅在 `key` 为 `Enter` 时调用 `submit` -->
<input @keyup.enter="submit" />
你可以使用以下系统按键修饰符来触发鼠标或键盘事件监听器,只有当按键被按下时才会触发。
.ctrl
.alt
.shift
.meta
<!-- Alt + Enter -->
<input @keyup.alt.enter="clear" />
<!-- Ctrl + 点击 -->
<div @click.ctrl="doSomething">Do something</div>
.exact
修饰符允许控制触发一个事件所需的确定组合的系统按键修饰符。
<!-- 当按下 Ctrl 时,即使同时按下 Alt 或 Shift 也会触发 -->
<button @click.ctrl="onClick">A</button>
<!-- 仅当按下 Ctrl 且未按任何其他键时才会触发 -->
<button @click.ctrl.exact="onCtrlClick">A</button>
<!-- 仅当没有按下任何系统按键时触发 -->
<button @click.exact="onClick">A</button>
鼠标修饰符
.left
.right
.middle