我喜爱 Vue 3 的 Composition API,它提供了两种办法来为 Vue 组件增加响应式状态:ref
和 reactive
。当你应用ref
时到处应用 .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) // ⚠️ total is still 20
在响应式零碎中,咱们冀望每当 price
或者 quantity
扭转时,total
就会被更新。然而 JavaScript 通常状况下并不会像预期的这样失效。
你兴许会嘀咕,为什么 Vue 须要响应式零碎?答案很简略:Vue 组件的状态由响应式 JavaScript 对象组成。当你批改这些对象时,视图或者依赖的响应式对象就会更新。
因而,Vue 框架必须实现另一种机制来跟踪局部变量的读和写,它是通过 拦挡对象属性的读写 来实现的。这样一来,Vue 就能够跟踪一个响应式对象的属性拜访以及更改。
因为浏览器的限度,Vue 2 专门应用 getters/setters 来拦挡属性。Vue 3 对响应式对象应用 Proxy,对 ref
应用 getters/setters。上面的伪代码展现了属性拦挡的基本原理;它解释了外围概念,并疏忽了许多细节和边缘状况:
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)
},
})
}
proxy
的 get
和set
办法通常被称为代理陷阱。
这里强烈建议浏览官网文档来查看无关 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
// Triggers watcher -> "{count: 0, nested: { count: 1} }"
}
限度
reactive()
API 有两个限度:
第一个限度是,它只实用于对象类型,比方对象、数组和汇合类型,如 Map
和Set
。它不适用于原始类型,比方 string
、number
或boolean
。
第二个限度是,从 reactive()
返回的代理对象与原始对象是不一样的。用 ===
操作符进行比拟会返回false
:
const plainJsObject = {}
const proxy = reactive(plainJsObject)
// proxy is NOT equal to the original plain JS object.
console.log(proxy === plainJsObject) // false
你必须始终保持对响应式对象的雷同援用,否则,Vue 无奈跟踪对象的属性。如果你试图将一个响应式对象的属性解构为局部变量,你可能会遇到这个问题:
const state = reactive({count: 0,})
// ⚠️ count is now a local variable disconnected from state.count
let {count} = state
count += 1 // ⚠️ Does not affect original state
侥幸的是,你能够首先应用 toRefs
将对象的所有属性转换为响应式的,而后你能够解构对象而不失落响应:
let state = reactive({count: 0,})
// count is a ref, maintaining reactivity
const {count} = toRefs(state)
如果你试图从新赋值 reactive
的值,也会产生相似的问题。如果你 ” 替换 ” 一个响应式对象,新的对象会笼罩对原始对象的援用,并且响应式连贯会失落:
const state = reactive({count: 0,})
watch(state, () => console.log(state), {deep: true})
// "{count: 0}"
// ⚠️ The above reference ({count: 0}) is no longer being tracked (reactivity connection is lost!)
state = reactive({count: 10,})
// ⚠️ The watcher doesn't fire
如果咱们传递一个属性到函数中,响应式连贯也会失落:
const state = reactive({count: 0,})
const useFoo = (count) => {
// ⚠️ Here count is a plain number and the useFoo composable
// cannot track changes to state.count
}
useFoo(state.count)
ref()
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}
你可能会问本人,ref()
如何能包容原始类型,因为咱们刚刚理解到 Vue 须要一个对象能力触发 get/set 代理陷阱。上面的伪代码展现了 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({}))
如果你想深刻理解,能够在源码中查看
ref()
的实现。
可怜的是,也不能对用 ref()
创立的响应式对象进行解构。这也会导致响应式失落:
import {ref} from 'vue'
const count = ref(0)
const countValue = count.value // ⚠️ disconnects reactivity
const {value: countDestructured} = count // ⚠️ disconnects reactivity
然而,如果将 ref
分组在一个一般的 JavaScript 对象中,就不会失落响应式:
const state = {count: ref(1),
name: ref('Michael'),
}
const {count, name} = state // still reactive
ref
也能够被传递到函数中而不失落响应式。
const state = {count: ref(1),
name: ref('Michael'),
}
const useFoo = (count) => {
/**
* The function receives a ref
* It needs to access the value via .value but it
* will retain the reactivity connection
*/
}
useFoo(state.count)
这种能力相当重要,因为它在将逻辑提取到组合式函数中时常常被应用。一个蕴含对象值的 ref
能够响应式地替换整个对象:
const state = {
count: 1,
name: 'Michael',
}
// Still reactive
state.value = {
count: 2,
name: 'Chris',
}
解包 refs()
在应用 ref
时到处应用 .value
可能很麻烦,但咱们能够应用一些辅助函数。
unref 实用函数
unref()是一个便捷的实用函数,在你的值可能是一个 ref
的状况下特地有用。在一个非 ref
上调用 .value
会抛出一个运行时谬误,unref()
在这种状况下就很有用:
import {ref, unref} from 'vue'
const count = ref(0)
const unwrappedCount = unref(count)
// same as isRef(count) ? count.value : count`
如果 unref()
的参数是一个 ref
,就会返回其外部值。否则就返回参数自身。这是的val = isRef(val) ? val.value : val
语法糖。
模板解包
当你在模板上调用 ref
时,Vue 会主动应用 unref()
进行解包。这样,你永远不须要在模板中应用 .value
进行拜访:
<script setup>
import {ref} from 'vue'
const count = ref(0)
</script>
<template>
<span>
<!-- no .value needed -->
{{count}}
</span>
</template>
只在
ref
是模板中的顶级属性时才失效。
侦听器
咱们能够间接传递一个 ref
作为侦听器的依赖:
import {watch, ref} from 'vue'
const count = ref(0)
// Vue automatically unwraps this ref for us
watch(count, (newCount) => console.log(newCount))
Volar
如果你正在应用 VS Code,你能够通过配置 Volar 扩大来主动地增加 .value
到ref
上。你能够在 Volar: Auto Complete Refs
设置中开启:
相应的 JSON 设置:
"volar.autoCompleteRefs": true
为了缩小 CPU 的应用,这个性能默认是禁用的。
比拟
让咱们总结一下 reactive
和ref
之间的区别:
reactive | ref |
---|---|
👎 只对对象类型起作用 | 👍对任何类型起作用 |
👍在 <script> 和<template> 中拜访值没有区别 |
👎拜访 <script> 和<template> 中的值的行为不同 |
👎从新赋值一个新的对象会 ” 断开 ” 响应式 | 👍对象援用能够被从新赋值 |
属性能够在没有 .value 的状况下被拜访 |
须要应用 .value 来拜访属性 |
👍援用能够通过函数进行传递 | |
👎解构的值不是响应式的 | |
👍与 Vue2 的 data 对象类似 |
我的观点
我最喜爱 ref
的中央是,如果你看到它的属性是通过 .value
拜访的,你就晓得它是一个响应式的值。如果你应用一个用 reactive
创立的对象,就不那么分明了:
anyObject.property = 'new' // anyObject could be a plain JS object or a reactive object
anyRef.value = 'new' // likely a ref
这个假如只有在你对 ref
有根本的理解,并且晓得你用 .value
来读取响应式变量时才无效。
如果你在应用 ref
,你应该尽量避免应用具备value
属性的非响应式对象:
const dataFromApi = {value: 'abc', name: 'Test'}
const reactiveData = ref(dataFromApi)
const valueFromApi = reactiveData.value.value // 🤮
如果你刚开始应用 Composition API,reactive
可能更直观,如果你试图将一个组件从 Options API 迁徙到 Composition API,它是相当不便的。reactive
的工作原理与 data
内的响应式属性十分类似:
<script>
export default {data() {
count: 0,
name: 'MyCounter'
},
methods: {increment() {this.count += 1;},
}
};
</script>
你能够简略地将 data
中的所有内容复制到 reactive
中,而后将这个组件迁徙到 Composition API 中:
<script setup>
setup() {
// Equivalent to "data" in Options API
const state = reactive({
count: 0,
name: 'MyCounter'
});
const {count, name} = toRefs(statee)
// Equivalent to "methods" in Options API
increment(username) {state.count += 1;}
}
</script>
比拟 ref 和 reactive
一个举荐的模式是在一个 reactive
对象中对 ref
分组:
const loading = ref(true)
const error = ref(null)
const state = reactive({
loading,
error,
})
// You can watch the reactive object...
watchEffect(() => console.log(state.loading))
// ...and the ref directly
watch(loading, () => console.log('loading has changed'))
setTimeout(() => {
loading.value = false
// Triggers both watchers
}, 500)
如果你不须要 state
对象自身的响应式,你能够在一个一般的 JavaScript 对象中进行分组。对 refs
进行分组的后果是一个繁多的对象,它更容易解决,并使你的代码放弃有序。你能够看到分组后的 refs
属于一起,并且是相干的。
这种模式也被用于像 Vuelidate 这样的库中,他们应用
reactive()
来设置验证的状态。
总结起来,社区中的最佳实际是默认应用ref
,在须要分组的时候应用reactive
。
总结
那么,你到底该应用 ref
还是reactive
?
我的倡议是默认应用ref
,当你须要分组时应用reactive
。Vue 社区也有同样的观点,但如果你决定默认应用reactive
,也齐全没有问题。
ref
和 reactive
都是在 Vue 3 中创立响应式变量的弱小工具。你甚至能够在没有任何技术缺点的状况下同时应用它们。只有你抉择你喜爱的那一个,并尽量在写代码时保持一致就能够了!
以上就是本文的全部内容,如果对你有所帮忙,欢送点赞、珍藏、转发~