使用Ref还是Reactive?

使用,ref,还是,reactive · 浏览次数 : 586

小编点评

** community 的最佳实践是默认使用 ref,在需要分组时使用 reactive。** **vue 3 中创建响应式变量的强大工具** * ref:一个用于指向响应式变量的 ref 的对象。 * reactive:一个用于处理响应式变量的 reactive 对象。 **使用 ref 的优点:** * 简化代码:使用 ref 代替对对象进行访问。 * 提升性能:ref 允许直接访问对象的值。 **使用 reactive 的优点:** * 允许访问响应式变量的值。 * 方便代码:使用 reactive 创建对象。 **建议:** * 默认使用 ref。 * 在需要分组时使用 reactive。 **总结:** * ref 和 reactive 在 Vue 3 中创建响应式变量的强大工具。 * 使用 ref 代替对对象进行访问。 * 使用 reactive 创建对象。 * 应该选择你喜欢的那一个,并尽量在写代码时保持一致。

正文

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

一句话总结:默认情况下使用ref,当你需要对变量分组时使用reactive

Vue3的响应式

在我解释refreactive之前,你应该了解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)
    },
  })
}

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.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扩展来自动地添加.valueref上。你可以在Volar: Auto Complete Refs设置中开启:

image.png

相应的JSON设置:

"volar.autoCompleteRefs": true

为了减少CPU的使用,这个功能默认是禁用的。

比较

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

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,也完全没有问题。

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

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

与使用Ref还是Reactive?相似的内容:

使用Ref还是Reactive?

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

2024已过半,还没试过在vue3中使用ioc容器吗?

Zova 是一款支持 IOC 容器的 Vue3 框架。有了 IOC 容器的加持,定义响应式状态不再需要ref/reactive,也不再需要ref.value

终于搞懂了!原来vue3中template使用ref无需.value是因为这个

前言 众所周知,vue3的template中使用ref变量无需使用.value。还可以在事件处理器中进行赋值操作时,无需使用.value就可以直接修改ref变量的值,比如:change msg。你猜vue是在编

React报错之Function components cannot have string refs

总览 当我们在一个函数组件中使用一个字符串作为ref时,会产生"Function components cannot have string refs"错误。为了解决该错误,使用useRef()钩子来得到一个可变的ref对象,这样你就可以在组件中作为ref使用。 这里有个示例用来展示错误是如何发生的

Vue3开发新范式,不用`ref/reactive`,不用`ref.value`

什么是Cabloy-Front? Cabloy-Front 是一款支持 IOC 容器的 Vue3 框架。不用ref/reactive,不用ref.value,不用pinia 与UI库的配合 Cabloy-Front 可以搭配任何 UI 库使用,并且内置了几款 UI 库的项目模版,便于开箱即用,包括:

在 Vue 中控制表单输入

Vue中v-model的思路很简单。定义一个可响应式的text(通常是一个ref),然后用v-model="text"将这个值绑定到一个input上。这就创造了一个双向的数据流: 用户在输入框中输入,text会发生变化。 text发生变化,输入框的值也随之变化。 让我们看看如何在Vue 3中使用v-

使用Cloudflare Worker加速docker镜像

前言 开发者越来越难了,现在国内的docker镜像也都️了,没有镜像要使用docker太难了,代理又很慢 现在就只剩下自建镜像的办法了 GitHub上有开源项目可以快速搭建自己的镜像库,不过还是有点麻烦,还好Cloudflare暂时还活着‍ 本文记录一下使用 Cloudf

使用C#/.NET解析Wiki百科数据实现获取历史上的今天

创建一个webapi项目做测试使用。 创建新控制器,搭建一个基础框架,包括获取当天日期、wiki的请求地址等 创建一个Http请求帮助类以及方法,用于获取指定URL的信息 使用http请求访问指定url,先运行一下,看看返回的内容。内容如图右边所示,实际上是一个Json数据。我们主要解析 大事记 部

Pybind11和CMake构建python扩展模块环境搭建

使用pybind11的CMake模板来创建拓展环境搭建 从Github上下载cmake_example的模板,切换分支,并升级pybind11子模块到最新版本 拉取pybind11使用cmake构建工具的模板仓库 git clone --recursive https://github.com/mr

说说RabbitMQ延迟队列实现原理?

使用 RabbitMQ 和 RocketMQ 的人是幸运的,因为这两个 MQ 自身提供了延迟队列的实现,不像用 Kafka 的同学那么苦逼,还要自己实现延迟队列。当然,这都是题外话,今天咱们重点来聊聊 RabbitMQ 延迟队列的实现原理,以及 RabbitMQ 实现延迟队列的优缺点有哪些? 很多人