响应式基础
API 偏好
本页以及后续指南中的许多其他章节,针对 Options API 和 Composition API 会包含不同内容。你当前的偏好是 Composition API。你可以使用左侧边栏顶部的“API 偏好”切换来切换 API 风格。
声明响应式状态
ref()
在 Composition API 中,推荐使用 ref() 函数来声明响应式状态:
js
import { ref } from 'vue'
const count = ref(0)ref() 接收参数,并返回一个被包裹在带有 .value 属性的 ref 对象中的值:
js
const count = ref(0)
console.log(count) // { value: 0 }
console.log(count.value) // 0
count.value++
console.log(count.value) // 1另见:Refs 的类型标注
要在组件模板中访问 refs,请在组件的 setup() 函数中声明并返回它们:
js
import { ref } from 'vue'
export default {
// `setup` 是一个专为 Composition API 设计的特殊钩子。
setup() {
const count = ref(0)
// 将 ref 暴露给模板
return {
count
}
}
}template
<div>{{ count }}</div>请注意,在模板中使用 ref 时,我们并不需要附加 .value。为了方便起见,refs 在模板中使用时会自动解包(但有一些注意事项)。
你也可以在事件处理器中直接修改 ref:
template
<button @click="count++">
{{ count }}
</button>对于更复杂的逻辑,我们可以在同一作用域中声明会修改 ref 的函数,并将其作为方法连同状态一起暴露出去:
js
import { ref } from 'vue'
export default {
setup() {
const count = ref(0)
function increment() {
// 在 JavaScript 中需要 `.value`
count.value++
}
// 不要忘记同时暴露这个函数。
return {
count,
increment
}
}
}随后,暴露的方法就可以作为事件处理器使用:
template
<button @click="increment">
{{ count }}
</button>这里有一个无需使用任何构建工具的在线示例,见 Codepen。
<script setup>
通过 setup() 手动暴露状态和方法可能会显得冗长。幸运的是,在使用单文件组件(SFC)时,可以避免这种写法。我们可以通过 <script setup> 简化用法:
vue
<script setup>
import { ref } from 'vue'
const count = ref(0)
function increment() {
count.value++
}
</script>
<template>
<button @click="increment">
{{ count }}
</button>
</template>在 <script setup> 中声明的顶层导入、变量和函数,会自动在同一组件的模板中可用。你可以把模板想象成在同一作用域中声明的 JavaScript 函数——它天然可以访问与之并列声明的所有内容。
TIP
在本指南的剩余部分中,我们主要会为 Composition API 代码示例使用 SFC + <script setup> 语法,因为这对 Vue 开发者来说是最常见的用法。
如果你没有使用 SFC,也仍然可以通过 setup() 选项使用 Composition API。
为什么使用 Refs?
你可能会想,为什么我们需要使用带有 .value 的 refs,而不是普通变量。要解释这一点,我们需要简要讨论 Vue 的响应式系统是如何工作的。
当你在模板中使用 ref,并在之后更改该 ref 的值时,Vue 会自动检测到变化并相应地更新 DOM。这之所以可行,是因为它采用了基于依赖追踪的响应式系统。当一个组件第一次渲染时,Vue 会追踪渲染过程中使用到的每一个 ref。之后,当某个 ref 发生变化时,它会为正在追踪它的组件触发重新渲染。
在标准 JavaScript 中,没有办法检测普通变量的访问或变更。不过,我们可以使用 getter 和 setter 方法拦截对象属性的 get 和 set 操作。
.value 属性让 Vue 有机会检测 ref 何时被访问或修改。在底层,Vue 会在 getter 中执行追踪,在 setter 中执行触发。从概念上说,你可以把 ref 看作如下对象:
js
// 伪代码,不是实际实现
const myRef = {
_value: 0,
get value() {
track()
return this._value
},
set value(newValue) {
this._value = newValue
trigger()
}
}refs 的另一个优点是,与普通变量不同,你可以把 ref 传入函数,同时仍然保留对最新值和响应式关联的访问。这在将复杂逻辑重构为可复用代码时尤其有用。
响应式系统将在响应式详解章节中更详细地讨论。
深层响应式
refs 可以持有任何值类型,包括深层嵌套的对象、数组,或像 Map 这样的 JavaScript 内置数据结构。
ref 会使其值变为深层响应式。这意味着即使你修改嵌套对象或数组,也可以预期变化会被检测到:
js
import { ref } from 'vue'
const obj = ref({
nested: { count: 0 },
arr: ['foo', 'bar']
})
function mutateDeeply() {
// 这些将按预期工作。
obj.value.nested.count++
obj.value.arr.push('baz')
}非原始值会通过 reactive() 转换为响应式代理,下面会对此进行讨论。
也可以通过浅层 refs 来关闭深层响应式。对于浅层 refs,只有对 .value 的访问才会被追踪以实现响应式。浅层 refs 可用于通过避免对大型对象的观察开销来优化性能,或用于内部状态由外部库管理的场景。
延伸阅读:
DOM 更新时机
当你修改响应式状态时,DOM 会自动更新。不过需要注意的是,DOM 更新并不是同步应用的。相反,Vue 会将它们缓冲到更新循环中的“下一个 tick”,以确保无论你进行了多少次状态变更,每个组件都只更新一次。
要在状态变更后等待 DOM 更新完成,可以使用全局 API nextTick():
js
import { nextTick } from 'vue'
async function increment() {
count.value++
await nextTick()
// 现在 DOM 已更新
}reactive()
还有另一种声明响应式状态的方式,即使用 reactive() API。不同于 ref 会将内部值包装在一个特殊对象中,reactive() 会让对象本身变为响应式:
js
import { reactive } from 'vue'
const state = reactive({ count: 0 })在模板中的用法:
template
<button @click="state.count++">
{{ state.count }}
</button>响应式对象是 JavaScript Proxy,其行为与普通对象完全一致。区别在于,Vue 能够拦截响应式对象所有属性的访问和变更,从而进行依赖追踪和触发更新。
reactive() 会深层转换对象:嵌套对象在被访问时也会被 reactive() 包裹。当 ref() 的值是一个对象时,内部也会调用它。类似于浅层 ref,也有 shallowReactive() API 可用于关闭深层响应性。
Reactive Proxy vs. Original
需要注意的是,reactive() 返回的是原始对象的 Proxy,它与原始对象并不相等:
js
const raw = {}
const proxy = reactive(raw)
// proxy 与原始对象并不相等。
console.log(proxy === raw) // false只有代理对象才是响应式的——修改原始对象不会触发更新。因此,在使用 Vue 的响应式系统时,最佳实践是 只使用状态的代理版本。
为了确保始终访问到同一个代理,对同一个对象调用 reactive() 总会返回同一个代理;对一个已有代理调用 reactive() 也会返回这个代理本身:
js
// 对同一个对象调用 reactive() 会返回同一个代理
console.log(reactive(raw) === proxy) // true
// 对代理对象调用 reactive() 会返回它自身
console.log(reactive(proxy) === proxy) // true这一规则同样适用于嵌套对象。由于深层响应性,响应式对象内部的嵌套对象也是代理:
js
const proxy = reactive({})
const raw = {}
proxy.nested = raw
console.log(proxy.nested === raw) // falsereactive() 的局限性
reactive() API 有几个局限:
值类型有限: 它只适用于对象类型(对象、数组,以及 集合类型,例如
Map和Set)。它不能持有 原始类型,例如string、number或boolean。无法整体替换对象: 由于 Vue 的响应式追踪是基于属性访问的,我们必须始终保持对响应式对象的同一个引用。这意味着我们不能轻易“替换”一个响应式对象,因为与第一个引用的响应式连接会丢失:
jslet state = reactive({ count: 0 }) // 上面的引用 ({ count: 0 }) 已经不再被追踪 // (响应式连接已丢失!) state = reactive({ count: 1 })不利于解构: 当我们把响应式对象中的原始类型属性解构到局部变量中,或者把该属性传递给一个函数时,就会失去响应式连接:
jsconst state = reactive({ count: 0 }) // 解构后,count 与 state.count 失去连接。 let { count } = state // 不会影响原始状态 count++ // 该函数接收的是一个普通数字, // 无法追踪对 state.count 的变化 // 为了保留响应性,我们必须传入整个对象 callSomeFunction(state.count)
由于这些局限,我们建议将 ref() 作为声明响应式状态的主要 API。
额外的 Ref 解包细节
作为响应式对象属性
当 ref 作为响应式对象的属性被访问或赋值时,会自动解包。换句话说,它的行为就像普通属性:
js
const count = ref(0)
const state = reactive({
count
})
console.log(state.count) // 0
state.count = 1
console.log(count.value) // 1如果将一个新的 ref 赋值给一个已关联现有 ref 的属性,它会替换旧的 ref:
js
const otherCount = ref(2)
state.count = otherCount
console.log(state.count) // 2
// 原始 ref 现在已与 state.count 断开连接
console.log(count.value) // 1ref 的解包只会发生在深层响应式对象内部。它被作为 浅层响应式对象 的属性访问时,不会发生解包。
数组和集合中的注意事项
与响应式对象不同,当 ref 被作为响应式数组的元素或 Map 这类原生集合类型的元素访问时,不会进行解包:
js
const books = reactive([ref('Vue 3 Guide')])
// 这里需要 .value
console.log(books[0].value)
const map = reactive(new Map([['count', ref(0)]]))
// 这里需要 .value
console.log(map.get('count').value)模板中解包的注意事项
模板中的 ref 解包仅适用于该 ref 是模板渲染上下文中的顶级属性。
在下面的例子中,count 和 object 是顶级属性,但 object.id 不是:
js
const count = ref(0)
const object = { id: ref(1) }因此,下面这个表达式会按预期工作:
template
{{ count + 1 }}而这个则 不会:
template
{{ object.id + 1 }}渲染结果将是 [object Object]1,因为在求值表达式时 object.id 没有被解包,仍然是一个 ref 对象。要修复这一点,我们可以将 id 解构为顶级属性:
js
const { id } = objecttemplate
{{ id + 1 }}现在渲染结果将是 2。
另一个需要注意的是,如果 ref 是文本插值(即 {{ }} 标签)最终求值的结果,那么它会被解包,因此下面会渲染为 1:
template
{{ object.id }}这只是文本插值的一项便捷特性,等同于 {{ object.id.value }}。