3

Vue3使用Ref还是Reactive?

 1 year ago
source link: https://www.fly63.com/article/detial/12459
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client
644b6097f2548.png

我喜欢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设置中开启:

644b60a052e8d.png

相应的JSON设置:

"volar.autoCompleteRefs": true
为了减少CPU的使用,这个功能默认是禁用的。

让我们总结一下reactive和ref之间的区别:

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 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中创建响应式变量的强大工具。你甚至可以在没有任何技术缺陷的情况下同时使用它们。只要你选择你喜欢的那一个,并尽量在写代码时保持一致就可以了!

原文:https://mokkapps.de/blog/ref-vs-reactive-what-to-choose-using-vue-3-composition-api/

链接: https://www.fly63.com/article/detial/12459


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK