我喜爱Vue 3的Composition API,它提供了两种办法来为Vue组件增加响应式状态:refreactive。当你应用ref时到处应用.value是很麻烦的,但当你用reactive创立的响应式对象进行重构时,也很容易失落响应性。 在这篇文章中,我将阐释你如何来抉择reactive以及ref

一句话总结:默认状况下应用ref,当你须要对变量分组时应用reactive

Vue3的响应式

在我解释refreactive之前,你应该理解Vue3响应式零碎的基本知识。

如果你曾经把握了Vue3响应式零碎是如何工作的,你能够跳过本大节。

很可怜,JavaScript默认状况下并不是响应式的。让咱们看看上面代码示例:

let price = 10.0const quantity = 2const total = price * quantityconsole.log(total) // 20price = 20.0console.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)    },  })}

proxygetset办法通常被称为代理陷阱。

这里强烈建议浏览官网文档来查看无关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有两个限度:

第一个限度是,它只实用于对象类型,比方对象、数组和汇合类型,如MapSet。它不适用于原始类型,比方stringnumberboolean

第二个限度是,从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.countlet { count } = statecount += 1 // ⚠️ Does not affect original state

侥幸的是,你能够首先应用toRefs将对象的所有属性转换为响应式的,而后你能够解构对象而不失落响应:

let state = reactive({  count: 0,})// count is a ref, maintaining reactivityconst { 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) // 0count.value++console.log(count.value) // 1state.value.count = 1console.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 reactivityconst { 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 reactivestate.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 uswatch(count, (newCount) => console.log(newCount))

Volar

如果你正在应用VS Code,你能够通过配置Volar扩大来主动地增加.valueref上。你能够在Volar: Auto Complete Refs设置中开启:

相应的JSON设置:

"volar.autoCompleteRefs": true
为了缩小CPU的应用,这个性能默认是禁用的。

比拟

让咱们总结一下reactiveref之间的区别:

reactiveref
 只对对象类型起作用对任何类型起作用
<script><template>中拜访值没有区别拜访<script><template>中的值的行为不同
从新赋值一个新的对象会"断开"响应式对象援用能够被从新赋值
属性能够在没有.value的状况下被拜访须要应用.value来拜访属性
援用能够通过函数进行传递
解构的值不是响应式的
与Vue2的data对象类似

我的观点

我最喜爱ref的中央是,如果你看到它的属性是通过.value拜访的,你就晓得它是一个响应式的值。如果你应用一个用reactive创立的对象,就不那么分明了:

anyObject.property = 'new' // anyObject could be a plain JS object or a reactive objectanyRef.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 directlywatch(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,也齐全没有问题。

refreactive都是在Vue 3中创立响应式变量的弱小工具。你甚至能够在没有任何技术缺点的状况下同时应用它们。只有你抉择你喜爱的那一个,并尽量在写代码时保持一致就能够了!

以上就是本文的全部内容,如果对你有所帮忙,欢送点赞、珍藏、转发~