【译】Ref vs. Reactive:使用Vue3组合式API该如何选择?

原文地址:Ref vs. Reactive: What to Choose Using Vue 3 Composition API?

官方文档相关章节:响应式基础深入响应式系统

本文参考官方文档结合个人理解做了部分修改,不足之处恳请批评指教!


我喜欢 Vue3 的组合式 API,但是它提供了两种响应式 state 方法:refreactive 。使用 refs 时到处需要 .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) // ⚠️ 结果仍旧是 20

在响应式系统中,我们期望 totalpricequantity 改变时更新。但是 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 有两个限制:

  1. 仅对对象类型有效(对象、数组和 MapSet 这样的集合类型),而对 stringnumberboolean 这样的基本类型无效。
  2. 经过 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

如果参数是 refundef() 返回其内部的值,否则就返回其本身。这是 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 的不同:

reactiveref
👎只对 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 可能更直观和方便实现迁移。reactivedata 字段内的响应式属性非常相似:

// 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 ,也是完全可以的。

refreactive 都是 Vue3 中非常有用的创建响应式变量的方法。你甚至可以在没有任何技术缺陷的情况下使用两者。只需要选择一个你最喜欢的方式,尝试在你的代码风格中保持一致!

如果你喜欢这篇文章,可以关注作者 @Mokkapps 获取更多内容,同时可以及时接收新的文章推送。

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Vue 3的组合式API是一种新的编程模式,它使得在Vue组件中可以更灵活地组织和复用逻辑。下面是对Vue 3组合式API的介绍: 1. Composition API组合式API):Vue 3中引入了Composition API,它允许我们将逻辑按照功能进行组合,而不是按照生命周期钩子进行划分。这样可以更好地组织和复用代码。 2. setup函数:在Vue 3中,我们需要在组件中使用setup函数来定义组合式API。setup函数在组件创建之前执行,并且接收两个参数:props和context。我们可以在setup函数中定义响应式数据、计算属性、方法等。 3. reactive函数:reactive函数是Vue 3中用来创建响应式数据的函数。我们可以使用reactive函数将普通对象转换为响应式对象,从而实现数据的双向绑定。 4. ref函数:ref函数是Vue 3中用来创建单个响应式数据的函数。与reactive函数不同,ref函数返回一个包装过的对象,我们需要通过.value属性来访问和修改数据。 5. computed函数:computed函数用来创建计算属性。与Vue 2中的计算属性类似,我们可以使用computed函数来定义一个依赖其他响应式数据的属性。 6. watch函数:watch函数用来监听响应式数据的变化。我们可以使用watch函数来执行一些副作用操作,比如发送网络请求或者更新DOM。 7. 生命周期钩子:在Vue 3中,生命周期钩子函数被废弃了,取而代之的是使用setup函数来处理组件的生命周期逻辑。我们可以在setup函数中使用onMounted、onUpdated等函数来模拟Vue 2中的生命周期钩子。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值