响应式基础
reactive()
我们可以使用reactive()
函数创建一个响应式对象或数组:
import { reactive } from 'vue' const state = reactive({ count: 0 })
响应式对象,其实是JavaScript Proxy
,其行为表现与一般对象相似。不同之处:Vue 能够跟踪响应式对象属性的访问与更改操作。
要在组件模板中使用响应式状态,请在setup()
函数中定义并返回。
import { reactive } from 'vue' export default { // `setup` 是一个专门用于组合式 API 的特殊钩子setup() { const state = reactive({ count: 0 }) // 暴露 state 到模板return { state } } }
template <div>{{ state.count }}</div>
我们也可以,在同一个作用域下,定义一个更新state的函数,并作为一个方法与state一起暴露出去:
import { reactive } from 'vue' export default {setup() { const state =reactive ({ count: 0 })function increment() { state.count++ } // 不要忘了同时暴露 increment 函数return { state, increment } } }
暴露的方法,通常会被用作事件监听器:
<button @click="increment "> {{ state.count }} </button>
<script setup>
在setup()
函数中手动暴露状态和方法可能非常繁琐。幸运的是,你可以通过使用构建工具来简化该操作。当使用单文件组件(SFC)时,我们可以使用<script setup>
来简化大量样板代码。
<script setup> import { reactive } from 'vue' const state = reactive({ count: 0 }) function increment() { state.count++ }</script> <template> <button @click="increment"> {{ state.count }} </button></template>
<script setup>
中的顶层的导入和变量声明,在同一组件的模板中,自动直接使用。
DOM 更新时机
当你更改响应式状态后,DOM 也会自动更新。然而,你得注意 DOM 的更新并不是同步的。相反,Vue 将缓冲它们直到更新周期的“next tick”执行,以确保无论你进行了多少次状态更改,每个组件都只需要更新一次。
若要等待一个状态改变后的 DOM 更新完成,你可以使用nextTick()
这个全局 API:
import { nextTick } from 'vue' function increment() { count.value++ nextTick(() => { // 访问更新后的 DOM }) }
深层响应性
在 Vue 中,状态都是默认深层响应式的。这意味着即使在更改深层次的对象或数组,你的改动也能被检测到。
import { reactive } from 'vue' const obj = reactive({nested : { count: 0 },arr : ['foo', 'bar'] }) function mutateDeeply() { // 以下都会按照期望工作 obj.nested.count++ obj.arr.push ('baz') }
你也可以直接创建一个浅层响应式对象。它们仅在顶层具有响应性,一般仅在某些特殊场景中需要。
响应式代理 vs.原始对象
值得注意的是,reactive()
返回的是一个源对象的Proxy,它和源对象是不全等的(!==
):
const raw = {} const proxy = reactive(raw) // 代理和原始对象不是全等的 console.log(proxy === raw) // false
只有代理是响应式的,更改原始的对象不会触发更新。因此,使用 Vue 的响应式系统的最佳实践是仅使用你声明对象的代理版本。
为保证访问代理的一致性,对同一个对象调用reactive()
会总是返回同样的代理,而对代理调用reactive()
则会返回它自己:
const raw = {} const proxy = reactive(raw) const proxy2 = reactive(raw) // 在同一个对象上调用 reactive() 会返回相同的代理 console.log(proxy === proxy2) // 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 的响应式系统是通过对象的属性(property)访问进行追踪的,因此我们必须始终保持对该响应式对象的相同引用。这意味着我们不可以随意地“替换”一个响应式对象,因为这将导致对初始引用的响应性连接丢失。更新响应式对象属性的值,不会丧失响应性。
let state =reactive ({ count: 0 }) // 把已经创建的具备响应式的变量 state ,重新赋值,state 会失去响应性。 state =reactive ({ count: 1 })
同时,这也意味着,当我们将响应式对象的属性赋值给一个新变量时,此新变量失去响应性。
const state = reactive({ count: 0 }) //把具备响应性 state.count 赋值给变量 n,那么 n 不具备响应性 let n = state.count // 变量 n 自增加,并不影响 state.count 状态 n++
解构响应式对象,至本地变量时,解构出来的变量失去响应性。
const state = reactive({ count: 0 }) // count 也和 state.count 失去了响应性连接 let { count } = state // 不会影响原状态 count++
将响应式对象的任何属性传入一个函数时,此对象将会失去响应性。
const state = reactive({ count: 0 }) // 更换响应式对象的属性的值,其对象的响应性不会丢失。 state.count = 10 // 对响应式对象传入一个函数后,state 对象会丧失响应性,并且将无法跟踪 state.count 的变化callSomeFunction (state.count)
ref()
为了解决reactive()
带来的限制,我们提供了另一个ref()
方法来帮我们创建响应式的ref,它可以装载任何值类型:
import { ref } from 'vue'const count = ref(0)
ref()
将传入参数的值,包装为一个带.value属性的对象:
const count = ref(0) console.log(count) // { value: 0 } console.log(count.value) // 0 count.value++ console.log(count.value) // 1
和响应式对象的属性类似,ref的.value属性也是响应式的。同时,当值为对象类型时,会用reactive()
自动转换它的.value。
一个包含对象类型值的ref,可以响应式地替换整个对象:
const objectRef = ref({ count: 0 }) // 这是响应式的替换 objectRef.value = { count: 1 }
ref被传递给函数或是从一般对象上被解构时,不会丢失响应性:
const obj = {foo : ref(1),bar : ref(2) } // 该函数接收一个 ref 需要通过 .value 取值。但它会保持响应性。callSomeFunction (obj.foo) // 解构赋值,仍然是响应式的 const { foo, bar } = obj
一言以蔽之,ref()
使我们能创造一种对对任意值的“引用”,并能够在不丢失响应性的前提下,传递这些引用。这个功能非常重要,因为它经常用于将逻辑提取到组合函数中。
ref 在模板中的解包
当ref在模板中作为顶层property被访问时,它们会被自动“解包”,所以不需要使用.value。下面是之前的计数器例子,用ref()
代替:
<script setup> import { ref } from 'vue' const count = ref(0) function increment() { count.value++ } </script> <template> <button @click="increment"> {{ count }} <!-- no .value needed --> </button> </template>
请注意,仅当ref是模板渲染上下文的顶层属性时才适用自动“解包”,访问深层级的ref则不会解包:
js const object = { foo: ref(1) } template {{ object.foo + 1 }} <!-- 无法自动解包 -->
foo是顶层属性,但object.foo不是。上面的表达式,不会像预期的那样工作。
渲染的结果会是一个[object Object]
,因为object.foo
是一个ref对象。我们可以通过让foo成为顶层 property 来解决这个问题:
js const object = { foo: ref(1) } const { foo } = object template {{ foo + 1 }} <!-- 自动解包 渲染结果将是 2-->
需要注意的是,如果一个ref是文本插值(即一个{{}}
符号)计算的最终值,它也将被解包。因此下面的渲染结果将为1
:
{{ object.foo }}
这只是文本插值的一个方便功能,相当于{{object.foo.value}}
。
ref 在响应式对象中的解包
当一个ref作为一个响应式对象的 property 被访问或更改时,它会自动解包,因此会表现得和一般的 property 一样:
const count = ref(0) const state = reactive({ count }) console.log(state.count) // 0 state.count = 1 console.log(count.value) // 1
如果将一个新的ref赋值给响应式对象某个已经为ref的属性,那么它会替换掉旧的ref:
const otherCount = ref(2) state.count = otherCount console.log(state.count) // 2 // 原来的 ref 现在已经和 state.count 脱去联系 console.log(count.value) // 1
只有当嵌套在一个深层响应式对象内时,才会发生ref解包。当起作为浅层响应式对象的属性被访问时不会解包。
数组和集合类型的 ref 解包
跟响应式对象不同,当 ref 作为响应式数组或像 Map 这种原生集合类型的元素被访问时,不会进行解包。
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使用.value是一个受限于 JavaScript 语言限制的缺点。不过在编译期间,自动在合适的位置上添加上.value可以改进开发体验。Vue 提供了一个语法糖,可以在编译时作相应转换,使得我们可以像这样书写上面的计数器示例:
<script setup> let count = $ref(0) function increment() { // no need for .value count++ } </script> <template> <button @click="increment">{{ count }}</button> </template>
请注意它仍处于实验性阶段,在最终提案落地前仍可能发生改动。