响应式基础 | Reactivity Fundamentals
API 参考 | API Preference
This page and many other chapters later in the guide contain different content for the Options API and the Composition API. Your current preference is Composition API. You can toggle between the API styles using the "API Preference" switches at the top of the left sidebar.
本页和后面很多页面中都分别包含了选项式 API 和组合式 API 的示例代码。现在你选择的是 组合式 API。你可以使用左侧侧边栏顶部的“API 风格偏好”开关在 API 风格之间切换。
声明响应式状态 | Declaring Reactive State
ref()
In Composition API, the recommended way to declare reactive state is using the ref()
function:
在组合式 API 中,推荐使用 ref()
函数来声明响应式状态:
js
import { ref } from 'vue'
const count = ref(0)
ref()
takes the argument and returns it wrapped within a ref object with a .value
property:
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
See also: Typing Refs
参考:为 refs 标注类型
To access refs in a component's template, declare and return them from a component's setup()
function:
要在组件模板中访问 ref,请从组件的 setup()
函数中声明并返回它们:
js
import { ref } from 'vue'
export default {
// `setup` is a special hook dedicated for the Composition API.
// `setup` 是一个特殊的钩子,专门用于组合式 API。
setup() {
const count = ref(0)
// expose the ref to the template
// 将 ref 暴露给模板
return {
count
}
}
}
template
<div>{{ count }}</div>
Notice that we did not need to append .value
when using the ref in the template. For convenience, refs are automatically unwrapped when used inside templates (with a few caveats).
注意,在模板中使用 ref 时,我们不需要附加 .value
。为了方便起见,当在模板中使用时,ref 会自动解包 (有一些注意事项)。
You can also mutate a ref directly in event handlers:
你也可以直接在事件监听器中改变一个 ref:
template
<button @click="count++">
{{ count }}
</button>
For more complex logic, we can declare functions that mutate refs in the same scope and expose them as methods alongside the state:
对于更复杂的逻辑,我们可以在同一作用域内声明更改 ref 的函数,并将它们作为方法与状态一起公开:
js
import { ref } from 'vue'
export default {
setup() {
const count = ref(0)
function increment() {
// .value is needed in JavaScript
// 在 JavaScript 中需要 .value
count.value++
}
// don't forget to expose the function as well.
// 不要忘记同时暴露 increment 函数
return {
count,
increment
}
}
}
Exposed methods can then be used as event handlers:
然后,暴露的方法可以被用作事件监听器:
template
<button @click="increment">
{{ count }}
</button>
Here's the example live on Codepen, without using any build tools.
这里是 Codepen 上的例子,没有使用任何构建工具。
<script setup>
Manually exposing state and methods via setup()
can be verbose. Luckily, it can be avoided when using Single-File Components (SFCs). We can simplify the usage with <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>
在演练场中尝试一下 | Try it in the Playground
Top-level imports, variables and functions declared in <script setup>
are automatically usable in the template of the same component. Think of the template as a JavaScript function declared in the same scope - it naturally has access to everything declared alongside it.
<script setup>
中的顶层的导入、声明的变量和函数可在同一组件的模板中直接使用。你可以理解为模板是在同一作用域内声明的一个 JavaScript 函数——它自然可以访问与它一起声明的所有内容。
TIP
For the rest of the guide, we will be primarily using SFC + <script setup>
syntax for the Composition API code examples, as that is the most common usage for Vue developers.
在指南的后续章节中,我们基本上都会在组合式 API 示例中使用单文件组件 + <script setup>
的语法,因为大多数 Vue 开发者都会这样使用。
If you are not using SFC, you can still use Composition API with the setup()
option.
如果你没有使用单文件组件,你仍然可以在 setup()
选项中使用组合式 API。
为什么要使用 ref?| 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.
你可能会好奇:为什么我们需要使用带有 .value
的 ref,而不是普通的变量?为了解释这一点,我们需要简单地讨论一下 Vue 的响应式系统是如何工作的。
When you use a ref in a template, and change 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 a re-render for components that are tracking it.
当你在模板中使用了一个 ref,然后改变了这个 ref 的值时,Vue 会自动检测到这个变化,并且相应地更新 DOM。这是通过一个基于依赖追踪的响应式系统实现的。当一个组件首次渲染时,Vue 会追踪在渲染过程中使用的每一个 ref。然后,当一个 ref 被修改时,它会触发追踪它的组件的一次重新渲染。
In standard JavaScript, there is no way to detect the access or mutation of plain variables. However, we can intercept the get and set operations of an object's properties using getter and setter methods.
在标准的 JavaScript 中,检测普通变量的访问或修改是行不通的。然而,我们可以通过 getter 和 setter 方法来拦截对象属性的 get 和 set 操作。
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:
该 .value
属性给予了 Vue 一个机会来检测 ref 何时被访问或修改。在其内部,Vue 在它的 getter 中执行追踪,在它的 setter 中执行触发。从概念上讲,你可以将 ref 看作是一个像这样的对象:
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.
另一个 ref 的好处是,与普通变量不同,你可以将 ref 传递给函数,同时保留对最新值和响应式连接的访问。当将复杂的逻辑重构为可重用的代码时,这将非常有用。
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
.
Ref 可以持有任何类型的值,包括深层嵌套的对象、数组或者 JavaScript 内置的数据结构,比如 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:
Ref 会使它的值具有深层响应性。这意味着即使改变嵌套对象或数组时,变化也会被检测到:
js
import { ref } from 'vue'
const obj = ref({
nested: { count: 0 },
arr: ['foo', 'bar']
})
function mutateDeeply() {
// these will work as expected.
// 以下都会按照期望工作
obj.value.nested.count++
obj.value.arr.push('baz')
}
Non-primitive values are turned into reactive proxies via reactive()
, which is discussed below.
非原始值将通过 reactive()
转换为响应式代理,该函数将在后面讨论。
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.
也可以通过 shallow ref 来放弃深层响应性。对于浅层 ref,只有 .value
的访问会被追踪。浅层 ref 可以用于避免对大型数据的响应性开销来优化性能、或者有外部库管理其内部状态的情况。
Further reading:
阅读更多:
DOM 更新时机 | 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.
当你修改了响应式状态时,DOM 会被自动更新。但是需要注意的是,DOM 更新不是同步的。Vue 会在“next tick”更新周期中缓冲所有状态的修改,以确保不管你进行了多少次状态修改,每个组件都只会被更新一次。
To wait for the DOM update to complete after a state change, you can use the nextTick() global API:
要等待 DOM 更新完成后再执行额外的代码,可以使用 nextTick() 全局 API:
js
import { nextTick } from 'vue'
async function increment() {
count.value++
await nextTick()
// Now the DOM is updated
// 现在 DOM 已经更新了
}
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:
还有另一种声明响应式状态的方式,即使用 reactive()
API。与将内部值包装在特殊对象中的 ref 不同,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.
响应式对象是 JavaScript 代理,其行为就和普通对象一样。不同的是,Vue 能够拦截对响应式对象所有属性的访问和修改,以便进行依赖追踪和触发更新。
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()
将深层地转换对象:当访问嵌套对象时,它们也会被 reactive()
包装。当 ref 的值是一个对象时,ref()
也会在内部调用它。与浅层 ref 类似,这里也有一个 shallowReactive()
API 可以选择退出深层响应性。
Reactive Proxy vs. Original
It is important to note that the returned value from reactive()
is a Proxy of the original object, which is not equal to the original object:
值得注意的是,reactive()
返回的是一个原始对象的 Proxy,它和原始对象是不相等的:
js
const raw = {}
const proxy = reactive(raw)
// proxy is NOT equal to the original.
// 代理对象和原始对象不是全等的
console.log(proxy === raw) // false
Only the proxy is reactive - mutating the original object will not trigger updates. Therefore, the best practice when working with Vue's reactivity system is to exclusively use the proxied versions of your state.
只有代理对象是响应式的,更改原始对象不会触发更新。因此,使用 Vue 的响应式系统的最佳实践是仅使用你声明对象的代理版本。
To ensure consistent access to the proxy, calling reactive()
on the same object always returns the same proxy, and calling reactive()
on an existing proxy also returns that same proxy:
为保证访问代理的一致性,对同一个原始对象调用 reactive()
会总是返回同样的代理对象,而对一个已存在的代理对象调用 reactive()
会返回其本身:
js
// calling reactive() on the same object returns the same proxy
// 在同一个对象上调用 reactive() 会返回相同的代理
console.log(reactive(raw) === proxy) // true
// calling reactive() on a proxy returns itself
// 在一个代理上调用 reactive() 会返回它自己
console.log(reactive(proxy) === proxy) // true
This rule applies to nested objects as well. Due to deep reactivity, nested objects inside a reactive object are also proxies:
这个规则对嵌套对象也适用。依靠深层响应性,响应式对象内的嵌套对象依然是代理:
js
const proxy = reactive({})
const raw = {}
proxy.nested = raw
console.log(proxy.nested === raw) // false
reactive()
的局限性 | Limitations of reactive()
The reactive()
API has a few limitations:
reactive()
API 有一些局限性:
- 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:
不能替换整个对象:由于 Vue 的响应式跟踪是通过属性访问实现的,因此我们必须始终保持对响应式对象的相同引用。这意味着我们不能轻易地“替换”响应式对象,因为这样的话与第一个引用的响应性连接将丢失:
jslet state = reactive({ count: 0 }) // the above reference ({ count: 0 }) is no longer being tracked // (reactivity connection is lost!) // 上面的 ({ count: 0 }) 引用将不再被追踪 // (响应性连接已丢失!) state = reactive({ count: 1 })
- Not destructure-friendly: when we destructure a reactive object's primitive type 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. // 当解构时,count 已经与 state.count 断开连接 let { count } = state // does not affect original 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 // 该函数接收到的是一个普通的数字 // 并且无法追踪 state.count 的变化 // 我们必须传入整个对象以保持响应性 callSomeFunction(state.count)
Due to these limitations, we recommend using ref()
as the primary API for declaring reactive state.
由于这些限制,我们建议使用 ref()
作为声明响应式状态的主要 API。
额外的 ref 解包细节 | Additional Ref Unwrapping Details
作为 reactive 对象的属性 | 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 :
一个 ref 会在作为响应式对象的属性被访问或修改时自动解包。换句话说,它的行为就像一个普通的属性:
js
const count = ref(0)
const state = reactive({
count
})
console.log(state.count) // 0
state.count = 1
console.log(count.value) // 1
If a new ref is assigned to a property linked to an existing ref, it will replace the old ref:
如果将一个新的 ref 赋值给一个关联了已有 ref 的属性,那么它会替换掉旧的 ref:
js
const otherCount = ref(2)
state.count = otherCount
console.log(state.count) // 2
// original ref is now disconnected from state.count
// 原始 ref 现在已经和 state.count 失去联系
console.log(count.value) // 1
Ref unwrapping only happens when nested inside a deep reactive object. It does not apply when it is accessed as a property of a shallow reactive object.
只有当嵌套在一个深层响应式对象内时,才会发生 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
:
与 reactive 对象不同的是,当 ref 作为响应式数组或原生集合类型 (如 Map
) 中的元素被访问时,它不会被解包:
js
const books = reactive([ref('Vue 3 Guide')])
// need .value here
// 这里需要 .value
console.log(books[0].value)
const map = reactive(new Map([['count', ref(0)]]))
// need .value here
// 这里需要 .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.
在模板渲染上下文中,只有顶级的 ref 属性才会被解包。
In the example below, count
and object
are top-level properties, but object.id
is not:
在下面的例子中,count
和 object
是顶级属性,但 object.id
不是:
js
const count = ref(0)
const object = { id: ref(1) }
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:
渲染的结果将是 [object Object]1
,因为在计算表达式时 object.id
没有被解包,仍然是一个 ref 对象。为了解决这个问题,我们可以将 id
解构为一个顶级属性:
js
const { id } = object
template
{{ id + 1 }}
Now the render result will be 2
.
现在渲染的结果将是 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
:
另一个需要注意的点是,如果 ref 是文本插值的最终计算值 (即 {{ }}
标签),那么它将被解包,因此以下内容将渲染为 1
:
template
{{ object.id }}
This is just a convenience feature of text interpolation and is equivalent to {{ object.id.value }}
.
该特性仅仅是文本插值的一个便利特性,等价于 {{ object.id.value }}
。