大家好,这里是大家的林语冰。本期分享的是 Vue 官方新闻维护者的一篇关于 ref
和 reactive
如何选择的博客。
免责声明
本文属于是语冰的直男翻译了属于是,仅供粉丝参考,英文原味版请临幸 Ref vs. Reactive: What to Choose Using Vue 3 Composition API?。
我喜欢 Vue 3 的组合式 API,但它提供了两种向 Vue 组件添加响应式状态的方法:ref
和 reactive
。在任何使用 ref
的地方使用 .value
可能会很麻烦,但是在解构 reactive
创建的响应式对象时也很容易失去响应性。
在本文中,我将解释何时选择 reactive
、ref
,或者两者兼得。
TL;DR:默认使用
ref
,需要对事物分组时使用reactive
。
Vue 3 中的响应性
在我解释 ref
和 reactive
之前,您应该了解 Vue 3 响应性系统的基础知识。
如果您已经知道 Vue 3 响应性系统的工作机制,可以跳过此章节。
不幸的是,JS(JavaScript)默认是非响应式的。让我们瞄一眼下面的代码示例:
js
let price = 10.0
const quantity = 2
const total = price * quantity
console.log(total) // 20
price = 20.0
console.log(total) // ⚠️ total 仍然是 20
在响应性系统中,我们期望每当 price
或 quantity
变化时 total
也会更新。但是 JS 通常不是这样工作的。
您可能会问自己,为什么 Vue 需要响应性系统?答案很简单:Vue 组件的状态由响应式 JS 对象组成。当您修改它们时,视图或依赖的响应式对象将更新。
因此,Vue 框架必须实现另一种机制来跟踪局部变量的读写,这是通过拦截对象属性的读写来完成的。这样,Vue 就可以跟踪响应式对象的属性访问和变更。
由于浏览器的限制,Vue 2 专门使用 getters/setter 来拦截属性。Vue 3 对响应式对象使用 Proxy
,对 getter/setter 使用 ref
。下述伪代码显示了属性拦截的基础知识;它应该解释了核心概念,且忽略了一大坨细节和极端情况:
js
function reactive(obj) {
return new Proxy(obj, {
get(target, key) {
track(target, key)
return target[key]
},
set(target, key, value) {
target[key] = value
trigger(target, key)
}
})
}
代理的 get/set
方法通常称为代理陷阱(proxy traps)。
我建议临幸官方文档,了解有关 Vue 响应性系统的更多细节。
reactive()
现在我们来分析一下如何使用 Vue 3 的 reactive()
函数来声明一个响应式状态:
js
import { reactive } from 'vue'
const state = reactive({ count: 0 })
默认情况下,此状态是深度响应式的。如果您改变了嵌套的数组或对象,这些变化会被 Vue 检测到:
js
import { reactive } from 'vue'
const state = reactive({
count: 0,
nested: { count: 0 }
})
watch(state, () => console.log(state))
// "{ count: 0, nested: { count: 0 } }"
const incrementNestedCount = () => {
state.nested.count += 1
// 触发侦听器 -> "{ count: 0, nested: { count: 1 } }"
}
reactive() 的局限性
reactive()
API 有两大限制:
第一个限制是它仅适用于 对象类型(比如对象、数组)和集合类型(比如 Map
和 Set
)。它不适用于 string
、number
或 boolean
等原始类型。
第二个限制是 reactive()
返回的代理对象与原始对象的标识不同 。使用 ===
运算符比较会返回 false
:
js
const plainJsObject = {}
const proxy = reactive(plainJsObject)
// 代理不等于原始的纯 JS 对象。
console.log(proxy === plainJsObject) // false
您必须始终保留对响应式对象的相同引用,否则 Vue 无法跟踪对象的属性。如果您尝试将响应式对象的属性解构为局部变量,那么您可能会遭遇此问题:
js
const state = reactive({
count: 0
})
// ⚠️ count 现在是一个与 state.count 失联的局部变量
let { count } = state
count += 1 // ⚠️ 不影响原始状态
幸运的是,您可以先诉诸 toRefs
将对象的所有属性转换为 ref
,然后您就可以在不失去响应性的情况下解构:
js
let state = reactive({
count: 0
})
// count 是一个 ref,它具备响应性
const { count } = toRefs(state)
如果您尝试给 reactive
的值重新赋值,也会出现此类问题。如果您"替换"了响应式对象,那么新对象会覆盖原始对象的引用,并且响应式连接将丢失:
js
const state = reactive({
count: 0
})
watch(state, () => console.log(state), { deep: true })
// "{ count: 0 }"
// ⚠️ 不再追踪上述引用 ({ count: 0 })(响应性连接已丢失!)
state = reactive({
count: 10
})
// ⚠️ 不会触发侦听器
如果我们将属性传递给函数,那么响应性连接也会丢失:
js
const state = reactive({
count: 0
})
const useFoo = count => {
// ⚠️ 这里 count 是纯粹的数字
// 组合式函数 useFoo 不会追踪 state.count 的变化
}
useFoo(state.count)
ref()
Vue 提供了 ref()
函数来解决 reactive()
的局限性。
ref()
不受限于对象类型,而可以保存任何值类型:
js
import { ref } from 'vue'
const count = ref(0)
const state = ref({ count: 0 })
要读写使用 ref()
创建的响应式变量,您需要通过 .value
属性访问它:
js
const count = ref(0)
const state = ref({ count: 0 })
console.log(count) // { value: 0 }
console.log(count.value) // 0
count.value++
console.log(count.value) // 1
state.value.count = 1
console.log(state.value) // { count: 1 }
您可能会问自己 ref()
如何保存原始类型,因为我们刚刚了解到 Vue 需要一个对象才能触发 get/set 的代理陷阱。下述伪代码展示了 ref
幕后的极简逻辑:
js
function ref(value) {
const refObject = {
get value() {
track(refObject, 'value')
return value
},
set value(newValue) {
value = newValue
trigger(refObject, 'value')
}
}
return refObject
}
保存对象类型时,ref
自动使用 reactive()
转换其 .value
:
js
ref({}) ~= ref(reactive({}))
如果您想深度学习,可以瞄一眼 Vue 源码的
ref()
实现。
不幸的是,解构 ref()
创建的响应式对象也是不可能事件。这会导致响应性丢失:
js
import { ref } from 'vue'
const count = ref(0)
const countValue = count.value // ⚠️ 响应性失联
const { value: countDestructured } = count // ⚠️ 响应性失联
虽然但是,如果将 ref
分组到普通的 JS 对象中,那么响应性不会丢失:
js
const state = {
count: ref(1),
name: ref('Michael')
}
const { count, name } = state // 仍是响应式
ref
也可以传递到函数中,而不会丢失响应性。
js
const state = {
count: ref(1),
name: ref('Michael')
}
const useFoo = count => {
/**
* 此函数接收一个 ref
* 它需要诉诸 .value 来访问值
* 但它会保持响应性连接
*/
}
useFoo(state.count)
此功能十分重要,因为它在将逻辑提取到组合式函数中时频繁使用。
包含一个对象值的 ref
可以响应式替换整个对象:
js
const state = {
count: 1,
name: 'Michael'
}
// 仍是响应式
state.value = {
count: 2,
name: 'Chris'
}
ref() 解包
使用 ref
时,到处使用 .value
可能会很头大,但我们可以使用某些辅助功能。
unref 工具函数
unref()
是一个便捷的工具函数,如果您的值可能是 ref
时它尤其给力。非 ref
值调用 .value
会引发运行时错误,unref()
在这种情况下会派上用场:
js
import { ref, unref } from 'vue'
const count = ref(0)
const unwrappedCount = unref(count)
// same as isRef(count) ? count.value : count`
如果参数是 ref
,那么 unref
会返回内部的值,否则返回该参数本身。这是 val = isRef(val) ? val.value : val
的语法糖函数。
模板解包
当您在模板中调用 ref
时,Vue 会自动使用 unref()
"解包" ref
。这样,您就不需要在模板中使用 .value
:

当且仅当
ref
是模板中的顶级属性时,这才奏效。
侦听器
我们可以直接传递 ref
作为侦听器的依赖:
js
import { watch, ref } from 'vue'
const count = ref(0)
// Vue 会自动为我们解包此 ref
watch(count, newCount => console.log(newCount))
Volar
如果您是 VS Code 爱好者,那么您可以将 Volar 扩展配置为自动为 ref
添加 .value
。您可以在设置的 Volar: Auto Complete Refs
启动它:

对应的 JSON 设置是:
json
"volar.autoCompleteRefs": true
为了减少 CPU 的占用,此功能默认禁用。
总结 reactive() 和 ref() 的异同点
让我们总结一下 reactive()
和 ref()
的异同点:
reactive |
ref |
---|---|
👎 仅适用于对象类型 | 👍 适用于任何值 |
👍 在 <template> 和 <script> 中访问值没有区别 |
👎 在 <script> 和 <template> 中访问值的行为不同 |
👎 重新赋值给新对象会"断开"响应性 | 👍 对象引用可以重新赋值 |
不用通过 .value 访问属性 |
需要通过 .value 访问属性 |
--- | 👍 引用可以跨函数传递 |
👎 解构值是非响应式的 | --- |
👍 类似于 Vue 2 的 data 对象 | --- |
个人心证
我最喜欢的 ref
的一点是,如果您看到它的属性是通过 .value
访问的,您就知道它是一个响应值。如果您使用 reactive
创建的对象,那么绝非一目了然:
js
anyObject.property = 'new' // anyObject 可能是普通 JS 对象或响应式对象
anyRef.value = 'new' // 可能是 ref
当且仅当您对 ref
有基本了解,且知道您是在使用 .value
读取响应式变量时,此假设才有效。
如果您正在使用 ref
,那么您应该尽量避免使用具有 .value
属性的非响应式对象:
js
const dataFromApi = { value: 'abc', name: 'Test' }
const reactiveData = ref(dataFromApi)
const valueFromApi = reactiveData.value.value // 🤮
如果您不熟悉组合式 API,reactive
可能会更符合直觉,并且如果您尝试将组件从选项 API 迁移到组合式 API,它会十分便捷。reactive
的工作方式与 data
字段中的响应式属性十分雷同:

为了将此组件迁移到组合式 API,您只需将所有内容从 data
复制到 reactive
中:

组合 ref 和 reactive
一种推荐的模式是将 ref
分组到 reactive
对象中:
js
const loading = ref(true)
const error = ref(null)
const state = reactive({
loading,
error
})
// 您可以侦听响应式对象...
watchEffect(() => console.log(state.loading))
// ...且直接使用 ref
watch(loading, () => console.log('loading has changed'))
setTimeout(() => {
loading.value = false
// 同时触发两个侦听器
}, 500)
如果您不需要 state
对象本身的响应性,您可以将 ref
分组到一个普通的 JS 对象中。
将 ref
分组到更易于处理的对象,使代码易于维护。一目了然,您可以看到分组的 ref
梦幻联动且互相关联。
此模式也用于 Vuelidate 等库,它们使用
reactive()
来设置验证的状态。
Vue 社区的观点
了不起的 Michael Thiessen 写了一篇关于此话题的精彩且深入的文章,并收集了 Vue 社区名人的意见。
总而言之,它们都默认使用 ref
,并在需要对事物分组时使用 reactive
。
完结撒花
那么,您应该使用 ref
还是 reactive
呢?
我的建议是默认使用 ref
,并在需要对事物分组时使用 reactive
。Vue 社区英雄所见略同,但如果您决定默认使用 reactive
,那也问题不大。
ref
和 reactive
两者都是在 Vue 3 中创建响应式变量的强大工具。您甚至可以在没有任何技术短板的情况下同时使用它们。只需选择您喜欢的那个,并尝试在编写代码的方式上保持一致!
您现在收看的是前端翻译计划,学废了的小伙伴可以订阅此专栏合集,我们每天佛系投稿,欢迎持续关注前端生态。谢谢大家的点赞,掰掰~
