声明响应式状态
ref()
在组合式 API 中,推荐使用 ref() 函数来声明响应式状态:
import { ref } from 'vue' const count = ref(0)
ref()
接收参数,并将其包裹在一个带有 .value
属性的 ref 对象中返回:
const count = ref(0) console.log(count) // { value: 0 } console.log(count.value) // 0 count.value++ console.log(count.value) // 1
要在组件模板中访问 ref,请从组件的 setup()
函数中声明并返回它们:
import { ref } from 'vue' export default { // `setup` 是一个特殊的钩子,专门用于组合式 API。 setup() { const count = ref(0) // 将 ref 暴露给模板 return { count } } }<div>{{ count }}</div>
注意,在模板中使用 ref 时,我们不需要附加 .value
。为了方便起见,当在模板中使用时,ref 会自动解包 (有一些注意事项)。
你也可以直接在事件监听器中改变一个 ref:
<button @click="count++"> {{ count }} </button>
对于更复杂的逻辑,我们可以在同一作用域内声明更改 ref 的函数,并将它们作为方法与状态一起公开:
import { ref } from 'vue' export default { setup() { const count = ref(0) function increment() { // 在 JavaScript 中需要 .value count.value++ } // 不要忘记同时暴露 increment 函数 return { count, increment } } }
然后,暴露的方法可以被用作事件监听器:
<button @click="increment"> {{ count }} </button>
<script setup>
在 setup()
函数中手动暴露大量的状态和方法非常繁琐。幸运的是,我们可以通过使用单文件组件 (SFC) 来避免这种情况。我们可以使用 <script setup>
来大幅度地简化代码
<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 函数——它自然可以访问与它一起声明的所有内容。
为什么要使用 ref?
你可能会好奇:为什么我们需要使用带有 .value
的 ref,而不是普通的变量?为了解释这一点,我们需要简单地讨论一下 Vue 的响应式系统是如何工作的。
当你在模板中使用了一个 ref,然后改变了这个 ref 的值时,Vue 会自动检测到这个变化,并且相应地更新 DOM。这是通过一个基于依赖追踪的响应式系统实现的。当一个组件首次渲染时,Vue 会追踪在渲染过程中使用的每一个 ref。然后,当一个 ref 被修改时,它会触发追踪它的组件的一次重新渲染。
在标准的 JavaScript 中,检测普通变量的访问或修改是行不通的。然而,我们可以通过 getter 和 setter 方法来拦截对象属性的 get 和 set 操作。
该 .value
属性给予了 Vue 一个机会来检测 ref 何时被访问或修改。在其内部,Vue 在它的 getter 中执行追踪,在它的 setter 中执行触发。从概念上讲,你可以将 ref 看作是一个像这样的对象:
// 伪代码,不是真正的实现 const myRef = { _value: 0, get value() { track() return this._value }, set value(newValue) { this._value = newValue trigger() } }
另一个 ref 的好处是,与普通变量不同,你可以将 ref 传递给函数,同时保留对最新值和响应式连接的访问。当将复杂的逻辑重构为可重用的代码时,这将非常有用。
该响应性系统在深入响应式原理章节中有更详细的讨论。
深层响应性
Ref 可以持有任何类型的值,包括深层嵌套的对象、数组或者 JavaScript 内置的数据结构,比如 Map
。
Ref 会使它的值具有深层响应性。这意味着即使改变嵌套对象或数组时,变化也会被检测到:
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() 转换为响应式代理,该函数将在后面讨论。
也可以通过 shallow ref 来放弃深层响应性。对于浅层 ref,只有 .value
的访问会被追踪。浅层 ref 可以用于避免对大型数据的响应性开销来优化性能、或者有外部库管理其内部状态的情况。
DOM 更新时机
当你修改了响应式状态时,DOM 会被自动更新。但是需要注意的是,DOM 更新不是同步的。Vue 会在“next tick”更新周期中缓冲所有状态的修改,以确保不管你进行了多少次状态修改,每个组件都只会被更新一次。
要等待 DOM 更新完成后再执行额外的代码,可以使用 nextTick() 全局 API:
import { nextTick } from 'vue' async function increment() { count.value++ await nextTick() // 现在 DOM 已经更新了 }
reactive()
还有另一种声明响应式状态的方式,即使用 reactive()
API。与将内部值包装在特殊对象中的 ref 不同,reactive()
将使对象本身具有响应性:
import { reactive } from 'vue' const state = reactive({ count: 0 })
在模板中使用:
<button @click="state.count++"> {{ state.count }} </button>
响应式对象是 JavaScript 代理,其行为就和普通对象一样。不同的是,Vue 能够拦截对响应式对象所有属性的访问和修改,以便进行依赖追踪和触发更新。
reactive()
将深层地转换对象:当访问嵌套对象时,它们也会被 reactive()
包装。当 ref 的值是一个对象时,ref()
也会在内部调用它。与浅层 ref 类似,这里也有一个 shallowReactive() API 可以选择退出深层响应性。
Reactive Proxy vs. Original
值得注意的是,reactive()
返回的是一个原始对象的 Proxy,它和原始对象是不相等的
const raw = {} const proxy = reactive(raw) // 代理对象和原始对象不是全等的 console.log(proxy === raw) // false
只有代理对象是响应式的,更改原始对象不会触发更新。因此,使用 Vue 的响应式系统的最佳实践是仅使用你声明对象的代理版本。
为保证访问代理的一致性,对同一个原始对象调用 reactive()
会总是返回同样的代理对象,而对一个已存在的代理对象调用 reactive()
会返回其本身:
// 在同一个对象上调用 reactive() 会返回相同的代理 console.log(reactive(raw) === proxy) // true // 在一个代理上调用 reactive() 会返回它自己 console.log(reactive(proxy) === proxy) // true
这个规则对嵌套对象也适用。依靠深层响应性,响应式对象内的嵌套对象依然是代理:
const proxy = reactive({}) const raw = {} proxy.nested = raw console.log(proxy.nested === raw) // false
reactive()
的局限性
reactive()
API 有一些局限性:
- 有限的值类型:它只能用于对象类型 (对象、数组和如
Map
、Set
这样的集合类型)。它不能持有如string
、number
或boolean
这样的原始类型。 - 不能替换整个对象:由于 Vue 的响应式跟踪是通过属性访问实现的,因此我们必须始终保持对响应式对象的相同引用。这意味着我们不能轻易地“替换”响应式对象,因为这样的话与第一个引用的响应性连接将丢失:
let state = reactive({ count: 0 }) // 上面的 ({ count: 0 }) 引用将不再被追踪 // (响应性连接已丢失!) state = reactive({ count: 1 })
- 对解构操作不友好:当我们将响应式对象的原始类型属性解构为本地变量时,或者将该属性传递给函数时,我们将丢失响应性连接:
const state = reactive({ count: 0 }) // 当解构时,count 已经与 state.count 断开连接 let { count } = state // 不会影响原始的 state count++ // 该函数接收到的是一个普通的数字 // 并且无法追踪 state.count 的变化 // 我们必须传入整个对象以保持响应性 callSomeFunction(state.count)
- 由于这些限制,我们建议使用
ref()
作为声明响应式状态的主要 API。