计算属性
获取上一个值
- 仅 3.4+ 支持
如果需要,可以通过访问计算属性的 getter 的第一个参数来获取计算属性返回的上一个值:
xml
<script setup>
import { ref, computed } from 'vue'
const count = ref(2)
// 这个计算属性在 count 的值小于或等于 3 时,将返回 count 的值。
// 当 count 的值大于等于 4 时,将会返回满足我们条件的最后一个值
// 直到 count 的值再次小于或等于 3 为止。
const alwaysSmall = computed((previous) => {
if (count.value <= 3) {
return count.value
}
return previous
})
</script>
如果你正在使用可写的计算属性的话:
xml
<script setup>
import { ref, computed } from 'vue'
const count = ref(2)
const alwaysSmall = computed({
get(previous) {
if (count.value <= 3) {
return count.value
}
return previous
},
set(newValue) {
count.value = newValue * 2
}
})
</script>
最佳实践
Getter 不应有副作用
计算属性的 getter 应只做计算而没有任何其他的副作用,这一点非常重要,请务必牢记。举例来说,不要改变其他状态、在 getter 中做异步请求或者更改 DOM !一个计算属性的声明中描述的是如何根据其他值派生一个值。因此 getter 的职责应该仅为计算和返回该值。在之后的指引中我们会讨论如何使用侦听器根据其他响应式状态的变更来创建副作用。
避免直接修改计算属性值
从计算属性返回的值是派生状态。可以把它看作是一个"临时快照",每当源状态发生变化时,就会创建一个新的快照。更改快照是没有意义的,因此计算属性的返回值应该被视为只读的,并且永远不应该被更改------应该更新它所依赖的源状态以触发新的计算。
表单输入绑定
v-model
还可以用于各种不同类型的输入,<textarea>
、<select>
元素。它会根据所使用的元素自动使用对应的 DOM 属性和事件组合:
- 文本类型的
<input>
和<textarea>
元素会绑定value
property 并侦听input
事件; <input type="checkbox">
和<input type="radio">
会绑定checked
property 并侦听change
事件;<select>
会绑定value
property 并侦听change
事件。
.lazy
默认情况下,v-model
会在每次 input
事件后更新数据 (IME 拼字阶段的状态例外)。你可以添加 lazy
修饰符来改为在每次 change
事件后更新数据:
template
xml
<!-- 在 "change" 事件后同步更新而不是 "input" -->
<input v-model.lazy="msg" />
defineModel
v-model
可以在组件上使用以实现双向绑定。
从 Vue 3.4 开始,推荐的实现方式是使用 defineModel()
宏:
Child.vue
xml
<script setup>
const model = defineModel()
function update() {
model.value++
}
</script>
<template>
<div>Parent bound v-model is: {{ model }}</div>
<button @click="update">Increment</button>
</template>
父组件可以用 v-model
绑定一个值:
Parent.vue
ini
<Child v-model="countModel" />
defineModel
是一个便利宏。编译器将其展开为以下内容:
- 一个名为
modelValue
的 prop,本地 ref 的值与其同步; - 一个名为
update:modelValue
的事件,当本地 ref 的值发生变更时触发。
多个 v-model
绑定
利用刚才在 v-model
的参数小节中学到的指定参数与事件名的技巧,我们可以在单个组件实例上创建多个 v-model
双向绑定。
组件上的每一个 v-model
都会同步不同的 prop,而无需额外的选项:
ini
<UserName
v-model:first-name="first"
v-model:last-name="last"
/>
xml
<script setup>
const firstName = defineModel('firstName')
const lastName = defineModel('lastName')
</script>
<template>
<input type="text" v-model="firstName" />
<input type="text" v-model="lastName" />
</template>
watchEffect()
侦听器的回调使用与源完全相同的响应式状态是很常见的。例如下面的代码,在每当 todoId
的引用发生变化时使用侦听器来加载一个远程资源。
我们可以用 watchEffect
函数 来简化上面的代码。watchEffect()
允许我们自动跟踪回调的响应式依赖。上面的侦听器可以重写为:
javascript
watchEffect(async () => {
const response = await fetch(
`https://jsonplaceholder.typicode.com/todos/${todoId.value}`
)
data.value = await response.json()
})
这个例子中,回调会立即执行,不需要指定 immediate: true
。在执行期间,它会自动追踪 todoId.value
作为依赖(和计算属性类似)。每当 todoId.value
变化时,回调会再次执行。有了 watchEffect()
,我们不再需要明确传递 todoId
作为源值。
对于这种只有一个依赖项的例子来说,watchEffect()
的好处相对较小。但是对于有多个依赖项的侦听器来说,使用 watchEffect()
可以消除手动维护依赖列表的负担。此外,如果你需要侦听一个嵌套数据结构中的几个属性,watchEffect()
可能会比深度侦听器更有效,因为它将只跟踪回调中被使用到的属性,而不是递归地跟踪所有的属性。
watchEffect
仅会在其同步 执行期间,才追踪依赖。在使用异步回调时,只有在第一个 await
正常工作前访问到的属性才会被追踪。
副作用清理
有时我们可能会在侦听器中执行副作用,例如异步请求:
js
javascript
watch(id, (newId) => {
fetch(`/api/${newId}`).then(() => {
// 回调逻辑
})
})
但是如果在请求完成之前 id
发生了变化怎么办?当上一个请求完成时,它仍会使用已经过时的 ID 值触发回调。理想情况下,我们希望能够在 id
变为新值时取消过时的请求。
我们可以使用 onWatcherCleanup()
API 来注册一个清理函数,当侦听器失效并准备重新运行时会被调用:
javascript
import { watch, onWatcherCleanup } from 'vue'
watch(id, (newId) => {
const controller = new AbortController()
fetch(`/api/${newId}`, { signal: controller.signal }).then(() => {
// 回调逻辑
})
onWatcherCleanup(() => {
// 终止过期请求
controller.abort()
})
})
请注意,onWatcherCleanup
仅在 Vue 3.5+ 中支持,并且必须在 watchEffect
效果函数或 watch
回调函数的同步执行期间调用:你不能在异步函数的 await
语句之后调用它。
作为替代,onCleanup
函数还作为第三个参数传递给侦听器回调,以及 watchEffect
作用函数的第一个参数:
scss
watch(id, (newId, oldId, onCleanup) => {
// ...
onCleanup(() => {
// 清理逻辑
})
})
watchEffect((onCleanup) => {
// ...
onCleanup(() => {
// 清理逻辑
})
})
这在 3.5 之前的版本有效。此外,通过函数参数传递的 onCleanup
与侦听器实例相绑定,因此不受 onWatcherCleanup
的同步限制。
透传 Attributes
"透传 attribute"指的是传递给一个组件,却没有被该组件声明为 props 或 emits 的 attribute 或者 v-on
事件监听器。最常见的例子就是 class
、style
和 id
。
当一个组件以单个元素为根作渲染时,透传的 attribute 会自动被添加到根元素上。
如果你不想要 一个组件自动地继承 attribute,你可以在组件选项中设置 inheritAttrs: false
多根节点的 Attributes 继承
和单根节点组件有所不同,有着多个根节点的组件没有自动 attribute 透传行为。
异步组件
基本用法
在大型项目中,我们可能需要拆分应用为更小的块,并仅在需要时再从服务器加载相关组件。Vue 提供了 defineAsyncComponent
方法来实现此功能:
js
javascript
import { defineAsyncComponent } from 'vue'
const AsyncComp = defineAsyncComponent(() => {
return new Promise((resolve, reject) => {
// ...从服务器获取组件
resolve(/* 获取到的组件 */)
})
})
// ... 像使用其他一般组件一样使用 `AsyncComp`
如你所见,defineAsyncComponent
方法接收一个返回 Promise 的加载函数。这个 Promise 的 resolve
回调方法应该在从服务器获得组件定义时调用。你也可以调用 reject(reason)
表明加载失败。
ES 模块动态导入也会返回一个 Promise,所以多数情况下我们会将它和 defineAsyncComponent
搭配使用。类似 Vite 和 Webpack 这样的构建工具也支持此语法 (并且会将它们作为打包时的代码分割点),因此我们也可以用它来导入 Vue 单文件组件:
js
javascript
import { defineAsyncComponent } from 'vue'
const AsyncComp = defineAsyncComponent(() =>
import('./components/MyComponent.vue')
)
惰性激活
如果你正在使用服务器端渲染,这一部分才会适用。
在 Vue 3.5+ 中,异步组件可以通过提供激活策略来控制何时进行激活。
- Vue 提供了一些内置的激活策略。这些内置策略需要分别导入,以便在未使用时进行 tree-shake。
- 该设计有意保持在底层,以确保灵活性。将来可以在此基础上构建编译器语法糖,无论是在核心还是更上层的解决方案 (如 Nuxt) 中实现。
在空闲时进行激活
通过 requestIdleCallback
进行激活:
js
javascript
import { defineAsyncComponent, hydrateOnIdle } from 'vue'
const AsyncComp = defineAsyncComponent({
loader: () => import('./Comp.vue'),
hydrate: hydrateOnIdle(/* 传递可选的最大超时 */)
})
在可见时激活
通过 IntersectionObserver
在元素变为可见时进行激活。
js
javascript
import { defineAsyncComponent, hydrateOnVisible } from 'vue'
const AsyncComp = defineAsyncComponent({
loader: () => import('./Comp.vue'),
hydrate: hydrateOnVisible()
})
可以选择传递一个侦听器的选项对象值:
js
css
hydrateOnVisible({ rootMargin: '100px' })
在媒体查询匹配时进行激活
当指定的媒体查询匹配时进行激活。
js
javascript
import { defineAsyncComponent, hydrateOnMediaQuery } from 'vue'
const AsyncComp = defineAsyncComponent({
loader: () => import('./Comp.vue'),
hydrate: hydrateOnMediaQuery('(max-width:500px)')
})
交互时激活
当组件元素上触发指定事件时进行激活。完成激活后,触发激活的事件也将被重放。
js
javascript
import { defineAsyncComponent, hydrateOnInteraction } from 'vue'
const AsyncComp = defineAsyncComponent({
loader: () => import('./Comp.vue'),
hydrate: hydrateOnInteraction('click')
})
也可以是多个事件类型的列表:
js
css
hydrateOnInteraction(['wheel', 'mouseover'])
自定义策略
ts
typescript
import { defineAsyncComponent, type HydrationStrategy } from 'vue'
const myStrategy: HydrationStrategy = (hydrate, forEachElement) => {
// forEachElement 是一个遍历组件未激活的 DOM 中所有根元素的辅助函数,
// 因为根元素可能是一个片段而非单个元素
forEachElement(el => {
// ...
})
// 准备好时调用 `hydrate`
hydrate()
return () => {
// 如必要,返回一个销毁函数
}
}
const AsyncComp = defineAsyncComponent({
loader: () => import('./Comp.vue'),
hydrate: myStrategy
})