响应式基础
API 参考
本页和后面很多页面中都分别包含了选项式 API 和组合式 API 的示例代码。现在你选择的是 组合式 API。你可以使用左侧侧边栏顶部的 “API 风格偏好” 开关在 API 风格之间切换。
声明响应式状态
ref()
在组合式 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 标注类型
要访问组件模板中的 ref,请从组件的 setup()
函数中声明并返回它们:
js
import { ref } from 'vue'
export default {
// `setup` 是一个特殊的钩子,专门用于组合式 API。
setup() {
const count = ref(0)
// 将 ref 暴露给模板
return {
count
}
}
}
template
<div>{{ count }}</div>
注意,在模板中使用 ref 时,我们不需要附加 .value
。为了方便起见,当在模板中使用时,ref 会自动解包 (有一些注意事项)。
你也可以直接在事件监听器中改变一个 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++
}
// 不要忘记同时暴露 increment 函数
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
在指南的后续章节中,我们基本上都会在组合式 API 示例中使用单文件组件 + <script setup>
的语法,因为大多数 Vue 开发者都会这样使用。
如果你没有使用单文件组件,你仍然可以在 setup()
选项中使用组合式 API。
Why Refs?
You might be wondering why we need refs with the .value
instead of plain variables. To explain that, we will need to briefly discuss how Vue's reactivity system works.
When you use a ref in the template, and changes the ref's value later, Vue automatically detects the change and updates the DOM accordingly. This is made possible with a dependency-tracking based reactivity system. When a component is rendered for the first time, Vue tracks every ref that was used during the render. Later on, when a ref is mutated, it will trigger re-render for components that are tracking it.
In standard JavaScript, there is no way to detect the access or mutation of plain variables. But we can intercept a property's get and set operations.
The .value
property gives Vue the opportunity to detect when a ref has been accessed or mutated. Under the hood, Vue performs the tracking in its getter, and performs triggering in its setter. Conceptually, you can think of a ref as an object that looks like this:
js
// pseudo code, not actual implementation
const myRef = {
_value: 0,
get value() {
track()
return this._value
},
set value(newValue) {
this._value = newValue
trigger()
}
}
Another nice trait of refs is that unlike plain variables, you can pass refs into functions while retaining access to the latest value and the reactivity connection. This is particularly useful when refactoring complex logic into reusable code.
The reactivity system is discussed in more details in the Reactivity in Depth section.
Deep Reactivity
Refs can hold any value type, including deeply nested objects, arrays, or JavaScript built-in data structures like Map
.
A ref will make its value deeply reactive. This means you can expect changes to be detected even when you mutate nested objects or arrays:
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')
}
Non-primitive values are turned into reactive proxies via reactive()
, which is discussed below.
It is also possible to opt-out of deep reactivity with shallow refs. For shallow refs, only .value
access is tracked for reactivity. Shallow refs can be used for optimizing performance by avoiding the observation cost of large objects, or in cases where the inner state is managed by an external library.
Further reading:
DOM Update Timing
When you mutate reactive state, the DOM is updated automatically. However, it should be noted that the DOM updates are not applied synchronously. Instead, Vue buffers them until the "next tick" in the update cycle to ensure that each component updates only once no matter how many state changes you have made.
To wait for the DOM update to complete after a state change, you can use the nextTick() global API:
js
import { nextTick } from 'vue'
async function increment() {
count.value++
await nextTick()
// Now the DOM is updated
}
reactive()
There is another way to declare reactive state, with the reactive()
API. Unlike a ref which wraps the inner value in a special object, reactive()
makes an object itself reactive:
js
import { reactive } from 'vue'
const state = reactive({ count: 0 })
See also: Typing Reactive
Usage in template:
template
<button @click="state.count++">
{{ state.count }}
</button>
Reactive objects are JavaScript Proxies and behave just like normal objects. The difference is that Vue is able to intercept the access and mutation of all properties of a reactive object for reactivity tracking and triggering.
reactive()
converts the object deeply: nested objects are also wrapped with reactive()
when accessed. It is also called by ref()
internally when the ref value is an object. Similar to shallow refs, there is also the shallowReactive()
API for opting-out of deep reactivity.
Reactive Proxy vs. Original
值得注意的是,reactive()
返回的是一个原始对象的 Proxy,它和原始对象是不相等的:
js
const raw = {}
const proxy = reactive(raw)
// 代理对象和原始对象不是全等的
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) // false
reactive()
的局限性
The reactive()
API has a few limitations:
Limited value types: it only works for object types (objects, arrays, and collection types such as
Map
andSet
). It cannot hold primitive types such asstring
,number
orboolean
.Cannot replace entire object: since Vue's reactivity tracking works over property access, we must always keep the same reference to the reactive object. This means we can't easily "replace" a reactive object because the reactivity connection to the first reference is lost:
jslet state = reactive({ count: 0 }) // the above reference ({ count: 0 }) is no longer being tracked // (reactivity connection is lost!) state = reactive({ count: 1 })
Not destructure-friendly: when we destructure a reactive object's property into local variables, or when we pass that property into a function, we will lose the reactivity connection:
jsconst state = reactive({ count: 0 }) // count is disconnected from state.count when destructured. let { count } = state // 不会影响原始的 state count++ // the function receives a plain number and // won't be able to track changes to state.count // we have to pass the entire object in to retain reactivity callSomeFunction(state.count)
Due to these limitations, we recommend using ref()
as the primary API for declaring reactive state.
Additional Ref Unwrapping Details
As Reactive Object Property
A ref is automatically unwrapped when accessed or mutated as a property of a reactive object. In other words, it behaves like a normal property :
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) // 1
只有当嵌套在一个深层响应式对象内时,才会发生 ref 解包。当其作为浅层响应式对象的属性被访问时不会解包。
Caveat in Arrays and Collections
Unlike reactive objects, there is no unwrapping performed when the ref is accessed as an element of a reactive array or a native collection type like 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)
Caveat when Unwrapping in Templates
Ref unwrapping in templates only applies if the ref is a top-level property in the template render context.
In the example below, count
and object
are top-level properties, but object.id
is not:
js
const count = ref(0)
const object = { id: ref(0) }
Therefore, this expression works as expected:
template
{{ count + 1 }}
...while this one does NOT:
template
{{ object.id + 1 }}
The rendered result will be [object Object]1
because object.id
is not unwrapped when evaluating the expression and remains a ref object. To fix this, we can destructure id
into a top-level property:
js
const { id } = object
template
{{ id + 1 }}
Now the render result will be 2
.
Another thing to note is that a ref does get unwrapped if it is the final evaluated value of a text interpolation (i.e. a {{ }}
tag), so the following will render 1
:
template
{{ object.id }}
This is just a convenience feature of text interpolation and is equivalent to {{ object.id.value }}
.