原文地址:Ref vs. Reactive: What to Choose Using Vue 3 Composition API?
官网文档相干章节:响应式根底、深刻响应式零碎
本文参考官网文档联合集体了解做了局部批改,不足之处恳请批评指教!
我喜爱 Vue3 的组合式 API,然而它提供了两种响应式 state 办法:ref
和 reactive
。应用 refs 时到处须要 .value
显得很轻便,然而应用 reactive
又会很容易在解构时失落响应式。
在这篇文章中,我将解释该如何抉择应用 reactive
, ref
或者两者搭配应用。
太长不看版:默认应用
ref
,在须要分组应用时抉择reactive
。
Vue3 中的响应式
在解释 ref
和 reactive
之前,须要先简略理解一下 Vue3 中的响应式根底。
提醒
如果你曾经理解了 Vue3 中的响应式原理,能够跳过这一章节
原生 JavaScript 并没有提供任何响应式机制,来看上面这个例子:
let price = 10.0
const quantity = 2
const total = price * quantity
console.log(total) // 20
price = 20.0
console.log(total) // ⚠️ 后果仍旧是 20
在响应式零碎中,咱们冀望 total
在 price
或 quantity
扭转时更新。然而 JavaScript 通常不会这样做。
你可能会问本人,为什么 Vue 须要一个响应式零碎?这个答案很简略:Vue 组件的状态由响应式 JavaScript 对象组成。当你批改它们的时候,视图 view 或依赖它们的对象就会被更新。
因而,Vue 须要实现另一种机制来跟踪局部变量的读写,并且它是通过 拦挡对象属性的读和写 来实现的。这样,Vue 能够跟踪响应式对象属性的拜访和更改。
因为浏览器的限度,Vue2 应用 getter/setter 来拦挡属性的读写。Vue3 中应用 Proxy 来创立响应式对象,仅将 getter/setter 用于 ref。上面的伪代码阐明了它们是如何工作的:
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)
}
})
}
function ref(value) {
const refObject = {get value() {track(refObject, 'value')
return value
},
set value(newValue) {
value = newValue
trigger(refObject, 'value')
}
}
return refObject
}
这里和上面的代码都将以最简略的模式解释外围概念,因而省略了许多细节和边缘状况。
对于 Vue 中的响应性是如何工作的,举荐浏览官网文档:深刻响应式零碎。
reactive()
当初让咱们开始剖析如何应用 Vue3 的 reactive()
函数申明一个响应式状态:
import {reactive} from 'vue'
const state = reactive({count: 0})
状态默认都是深层响应式的,即便在更改深层次的对象或数组,你的改变也能被 Vue 检测到:
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
// 触发 watcher -> "{count: 0, nested: { count: 1} }"
}
reactive() 的限度
reactive()
API 有两个限度:
- 仅对对象类型无效(对象、数组和
Map
、Set
这样的汇合类型),而对string
、number
和boolean
这样的根本类型有效。 - 通过
reactive()
包装后的对象与原始对象援用地址不同,应用===
操作符比拟时会返回false
:
const plainJsObject = {}
const proxy = reactive(plainJsObject)
// proxy 与原始 js 对象不相等
console.log(proxy === plainJsObject) // false
必须放弃雷同的响应式对象的援用,否则 Vue 不能跟踪对象的属性。当你尝试解构响应式对象的属性到本地变量时会产生如下问题:
const state = reactive({count: 0,})
// ⚠️ count 与 state 失去链接,成为本地变量
let {count} = state
count += 1 // ⚠️ 不会影响原始状态
侥幸的是,你能够应用 toRefs
先将对象属性转换到 refs,而后就能够在放弃响应性的前提下将数据解构:
let state = reactive({count: 0,})
// count 是一个 ref,保留了响应性
const {count} = toRefs(state)
一个类似的问题产生在你尝试重新分配一个 reactive
值。如果你尝试“替换”一个响应式对象,新的对象会笼罩初试对象的援用,从而导致对初试援用的响应性连贯失落:
const state = reactive({count: 0,})
watch(state, () => console.log(state), {deep: true})
// "{count: 0}"
// ⚠️ 下面的 {{count: 0}} 不再被跟踪(失落了响应性)state = reactive({count: 10,})
// watcher 不会被触发
如果咱们将其中的属性传递给了函数,同样会使响应式连贯失落:
const state = reactive({count: 0,})
const useFoo = (count) => {
// ⚠️ 这里的 count 是一个一般的 number
// 不能跟踪 state.count 的扭转
}
useFoo(state.count)
ref()
reative()
的种种限度归根结底是因为 JavaScript 没有能够作用于所有值类型的“援用”机制。为此,Vue 提供了 ref()
函数用于解决上述 reactive()
的限度,能够创立应用任何值类型的响应式 ref:
import {ref} from 'vue'
const count = ref(0)
const state = ref({count: 0})
读和写由 ref()
创立的响应式变量时,须要应用 .value
属性:
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}
你可能会有疑难,因为咱们刚刚理解了 Vue 须要一个对象来触发 get/set 代理进行跟踪,所以 ref()
如何解决根本类型的值?上面的伪代码解释了 ref()
的根本逻辑:
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
的值:
ref({}) ~= ref(reactive({}))
如果想深刻理解,可参阅 Vue 中
ref()
局部的源码:ref.ts
可怜的是,同样不能解构由 ref()
创立的对象,因为也会造成响应性失落:
import {ref} from 'vue'
const count = ref(0)
const countValue = count.value // ⚠️ 失落了响应性
const {value: countDestructured} = count // ⚠️ 失落了响应性
然而如果 refs 从个别对象上被解构时,不会失落响应性:
const state = {count: ref(1),
name: ref('Michael'),
}
const {count, name} = state // 依然具备响应性
Refs 同样能够在不失落响应性的前提下以参数的模式传递给函数:
const state = {count: ref(1),
name: ref('Michael'),
}
const useFoo = (count) => {
/**
* 办法接管一个 ref
* 须要通过 .value 拿到值
* 然而这个值依然具备响应性
*/
}
useFoo(state.count)
这个性能很重要,因为它常常用于将逻辑提取到 组合函数 中。
一个蕴含对象类型值的 ref 能够响应式地替换整个对象:
const state = {
count: 1,
name: 'Michael',
}
// 依然具备响应性
state.value = {
count: 2,
name: 'Chris',
}
解包 refs()
在应用 refs 时,到处应用 .value
可能会很麻烦,但咱们能够应用一些辅助性的办法。
unref 办法
unref() 是一个不便的办法,如果一个变量的值是 ref
就能够施展出它的作用。在一个非 ref 的值中调用 .value
可能会抛出一个 runtime 谬误,这时 unref()
就派上了用场:
import {ref, unref} from 'vue'
const count = ref(0)
const unwrappedCount = unref(count)
// 相似于 isRef(count) ? count.value: count
如果参数是 ref
则 undef()
返回其外部的值,否则就返回其自身。这是 val = isRef(val) ? val.value : val
计算的一个语法糖。
在模板中的解包
当 ref 在模板中调用时,Vue 会应用 unref() 主动“解包”,所以不须要在模板中应用 .value
:
<script setup>
import {ref} from 'vue'
const count = ref(0)
</script>
<template>
<span>
<!-- 不须要应用 .value -->
{{count}}
</span>
</template>
留神:仅在 ref 在模板中作为顶层属性被拜访时起作用。
Watcher
咱们能够间接将 ref
作为 watcher 依赖传递:
import {watch, ref} from 'vue'
const count = ref(0)
// Vue 会主动解包 ref
watch(count, (newCount) => console.log(newCount))
Volar
如果你应用 VS Code,你能够应用 Volar 插件主动给 refs 增加 .value
。能够在设置中的 Volar: Auto Complete Refs
选项开启。
也能够在 JSON 设置中开启:
"volar.autoCompleteRefs": true
留神:为了缩小 CPU 的占用率,这个性能默认是敞开的。
reative() 和 ref() 的比照
让咱们来总结一下这两个 API 的不同:
reactive |
ref |
---|---|
👎只对 object 类型无效 | 👍对任意类型无效 |
👍在 <script> 和 <template> 中无差别应用 |
👎在 <script> 和 <template> 应用形式不同 |
👎重新分配一个新对象会失落响应性 | 👍可重新分配 object 援用 |
🫱可不通过 .value 拜访属性 |
🫱须要应用 .value 拜访属性 |
👍可将援用传递给函数 | |
👎解构时会失落响应性 | |
👍相似于 Vue2 的数据对象 |
我的观点
我比拟喜爱 ref
,如果你看到通过 .value
拜访其属性值,则晓得它是一个响应式的值。如果是应用 reactive
创立的对象,则不是那么分明晓得这是一个响应式对象:
anyObject.property = 'new' // anyObject 可能被当作一个一般的 JS 对象或者一个响应式的对象
anyRef.value = 'new' // 看起来是一个 ref
这个假如是对的,如果你对响应式根底有理解的话,就会晓得应该应用 .value
拜访响应式的值。
如果你应用 ref
,你应该防止应用一个无响应式的 key 值为 value
的属性:
const dataFromApi = {value: 'abc', name: 'Test'}
const reactiveData = ref(dataFromApi)
const valueFromApi = reactiveData.value.value // 🤮
如果你刚开始应用组合式 API,须要将我的项目从选项式 API 适度到组合式 API,reactive
可能更直观和不便实现迁徙。reactive
与 data
字段内的响应式属性十分类似:
// OptionsApiComponent.vue
<script>
export default {data() {
count: 0,
name: 'MyCounter'
},
methods: {increment() {this.count += 1;},
}
};
</script>
你只须要简略地将 data
中的任何数据复制到 reactive
中,就能够实现迁徙到组合式 API:
// CompositionApiComponent.vue
<script setup>
setup() {
// 与选项式 API 中的 'data' 相等
const state = reactive({
count: 0,
name: 'MyCounter'
});
const {count, name} = toRefs(statee)
// 与选项式 API 中的 'methods' 相等
increment(username) {state.count += 1;}
}
</script>
组合应用 ref 和 reactive
一种举荐应用的模式是在 reactive
对象中嵌套 refs:
const loading = ref(true)
const error = ref(null)
const state = reactive({
loading,
error,
})
// 能够 watch 响应式 object
watchEffect(() => console.log(state.loading))
// ... 和 ref
watch(loading, () => console.log('loading has changed'))
setTimeout(() => {
loading.value = false
// 触发所有的 watchers
}, 500)
如果你不须要响应式的 state
object,则能够将 refs 放入一般的 JavaScript object 中。
将 refs 放入一个组中便于解决同时放弃代码的直观整洁,能够间接看到组内的 refs 相关联性。
Vue 社区的观点
Michael Thiessen 写了一篇对于这个话题的精彩深刻的文章,收集了 Vue 社区中大佬们的观点:Ref vs. Reactive — Which is Best?
总的来说,他们 默认都应用 ref
,当须要分组时应用 reactive
。
论断
所以,你应该应用 ref
还是 reactive
?
我举荐默认应用 ref
,当须要分组的时候应用 reactive
。和 Vue 社区的观点一样,然而如果你决定默认应用 reactive
,也是齐全能够的。
ref
和 reactive
都是 Vue3 中十分有用的创立响应式变量的办法。你甚至能够在没有任何技术缺点的状况下应用两者。只须要抉择一个你最喜爱的形式,尝试在你的代码格调中保持一致!
如果你喜爱这篇文章,能够关注作者 @Mokkapps 获取更多内容,同时能够及时接管新的文章推送。