likes
comments
collection
share

ref vs reactive:Vue 3 组合式 API 的“零和博弈”(译)

作者站长头像
站长
· 阅读数 24

大家好,这里是大家的林语冰。本期分享的是 Vue 官方新闻维护者的一篇关于 refreactive 如何选择的博客。

免责声明

本文属于是语冰的直男翻译了属于是,仅供粉丝参考,英文原味版请临幸 Ref vs. Reactive: What to Choose Using Vue 3 Composition API?

我喜欢 Vue 3 的组合式 API,但它提供了两种向 Vue 组件添加响应式状态的方法:refreactive。在任何使用 ref 的地方使用 .value 可能会很麻烦,但是在解构 reactive 创建的响应式对象时也很容易失去响应性。

在本文中,我将解释何时选择 reactiveref,或者两者兼得。

TL;DR:默认使用 ref,需要对事物分组时使用 reactive

Vue 3 中的响应性

在我解释 refreactive 之前,您应该了解 Vue 3 响应性系统的基础知识。

如果您已经知道 Vue 3 响应性系统的工作机制,可以跳过此章节。

不幸的是,JS(JavaScript)默认是非响应式的。让我们瞄一眼下面的代码示例:

let price = 10.0
const quantity = 2

const total = price * quantity
console.log(total) // 20

price = 20.0
console.log(total) // ⚠️ total 仍然是 20

在响应性系统中,我们期望每当 pricequantity 变化时 total 也会更新。但是 JS 通常不是这样工作的。

您可能会问自己,为什么 Vue 需要响应性系统?答案很简单:Vue 组件的状态由响应式 JS 对象组成。当您修改它们时,视图或依赖的响应式对象将更新。

因此,Vue 框架必须实现另一种机制来跟踪局部变量的读写,这是通过拦截对象属性的读写来完成的。这样,Vue 就可以跟踪响应式对象的属性访问和变更。

由于浏览器的限制,Vue 2 专门使用 getters/setter 来拦截属性。Vue 3 对响应式对象使用 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)
    }
  })
}

代理的 get/set 方法通常称为代理陷阱(proxy traps)。

我建议临幸官方文档,了解有关 Vue 响应性系统的更多细节。

reactive()

现在我们来分析一下如何使用 Vue 3 的 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
  // 触发侦听器 -> "{ count: 0, nested: { count: 1 } }"
}

reactive() 的局限性

reactive() API 有两大限制:

第一个限制是它仅适用于对象类型(比如对象、数组)和集合类型(比如 MapSet)。它不适用于 stringnumberboolean 等原始类型。

第二个限制是 reactive() 返回的代理对象与原始对象的标识不同。使用 === 运算符比较会返回 false

const plainJsObject = {}
const proxy = reactive(plainJsObject)

// 代理不等于原始的纯 JS 对象。
console.log(proxy === plainJsObject) // false

您必须始终保留对响应式对象的相同引用,否则 Vue 无法跟踪对象的属性。如果您尝试将响应式对象的属性解构为局部变量,那么您可能会遭遇此问题:

const state = reactive({
  count: 0
})

// ⚠️ count 现在是一个与 state.count 失联的局部变量
let { count } = state

count += 1 // ⚠️ 不影响原始状态

幸运的是,您可以先诉诸 toRefs 将对象的所有属性转换为 ref,然后您就可以在不失去响应性的情况下解构:

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
})
// ⚠️ 不会触发侦听器

如果我们将属性传递给函数,那么响应性连接也会丢失:

const state = reactive({
  count: 0
})

const useFoo = count => {
  // ⚠️ 这里 count 是纯粹的数字
  // 组合式函数 useFoo 不会追踪 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({}))

如果您想深度学习,可以瞄一眼 Vue 源码的 ref() 实现。

不幸的是,解构 ref() 创建的响应式对象也是不可能事件。这会导致响应性丢失:

import { ref } from 'vue'

const count = ref(0)

const countValue = count.value // ⚠️ 响应性失联
const { value: countDestructured } = count // ⚠️ 响应性失联

虽然但是,如果将 ref 分组到普通的 JS 对象中,那么响应性不会丢失:

const state = {
  count: ref(1),
  name: ref('Michael')
}

const { count, name } = state // 仍是响应式

ref 也可以传递到函数中,而不会丢失响应性。

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'
}

ref() 解包

使用 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`

如果参数是 ref,那么 unref 会返回内部的值,否则返回该参数本身。这是 val = isRef(val) ? val.value : val 的语法糖函数。

模板解包

当您在模板中调用 ref 时,Vue 会自动使用 unref() “解包” ref。这样,您就不需要在模板中使用 .value

ref vs reactive:Vue 3 组合式 API 的“零和博弈”(译)

当且仅当 ref 是模板中的顶级属性时,这才奏效。

侦听器

我们可以直接传递 ref 作为侦听器的依赖:

import { watch, ref } from 'vue'

const count = ref(0)

// Vue 会自动为我们解包此 ref
watch(count, newCount => console.log(newCount))

Volar

如果您是 VS Code 爱好者,那么您可以将 Volar 扩展配置为自动为 ref 添加 .value。您可以在设置的 Volar: Auto Complete Refs 启动它:

ref vs reactive:Vue 3 组合式 API 的“零和博弈”(译)

对应的 JSON 设置是:

"volar.autoCompleteRefs": true

为了减少 CPU 的占用,此功能默认禁用。

总结 reactive() 和 ref() 的异同点

让我们总结一下 reactive()ref() 的异同点:

reactiveref
👎 适用于对象类型👍 适用于任何
👍 在 <template><script> 中访问值没有区别👎 在 <script><template> 中访问值的行为不同
👎 重新赋值给新对象会“断开”响应性👍 对象引用可以重新赋值
不用通过 .value 访问属性需要通过 .value 访问属性
---👍 引用可以跨函数传递
👎 解构值是非响应式的---
👍 类似于 Vue 2 的 data 对象---

个人心证

我最喜欢的 ref 的一点是,如果您看到它的属性是通过 .value 访问的,您就知道它是一个响应值。如果您使用 reactive 创建的对象,那么绝非一目了然:

anyObject.property = 'new' // anyObject 可能是普通 JS 对象或响应式对象

anyRef.value = 'new' // 可能是 ref

当且仅当您对 ref 有基本了解,且知道您是在使用 .value 读取响应式变量时,此假设才有效。

如果您正在使用 ref,那么您应该尽量避免使用具有 .value 属性的非响应式对象:

const dataFromApi = { value: 'abc', name: 'Test' }

const reactiveData = ref(dataFromApi)

const valueFromApi = reactiveData.value.value // 🤮

如果您不熟悉组合式 API,reactive 可能会更符合直觉,并且如果您尝试将组件从选项 API 迁移到组合式 API,它会十分便捷。reactive 的工作方式与 data 字段中的响应式属性十分雷同:

ref vs reactive:Vue 3 组合式 API 的“零和博弈”(译)

为了将此组件迁移到组合式 API,您只需将所有内容从 data 复制到 reactive 中:

ref vs reactive:Vue 3 组合式 API 的“零和博弈”(译)

组合 ref 和 reactive

一种推荐的模式是将 ref 分组到 reactive 对象中:

const loading = ref(true)
const error = ref(null)

const state = reactive({
  loading,
  error
})

// 您可以侦听响应式对象...
watchEffect(() => console.log(state.loading))

// ...且直接使用 ref
watch(loading, () => console.log('loading has changed'))

setTimeout(() => {
  loading.value = false
  // 同时触发两个侦听器
}, 500)

如果您不需要 state 对象本身的响应性,您可以将 ref 分组到一个普通的 JS 对象中。

ref 分组到更易于处理的对象,使代码易于维护。一目了然,您可以看到分组的 ref 梦幻联动且互相关联。

此模式也用于 Vuelidate 等库,它们使用 reactive() 来设置验证的状态。

Vue 社区的观点

了不起的 Michael Thiessen 写了一篇关于此话题的精彩且深入的文章,并收集了 Vue 社区名人的意见。

总而言之,它们都默认使用 ref,并在需要对事物分组时使用 reactive

完结撒花

那么,您应该使用 ref 还是 reactive 呢?

我的建议是默认使用 ref,并在需要对事物分组时使用 reactive。Vue 社区英雄所见略同,但如果您决定默认使用 reactive,那也问题不大。

refreactive 两者都是在 Vue 3 中创建响应式变量的强大工具。您甚至可以在没有任何技术短板的情况下同时使用它们。只需选择您喜欢的那个,并尝试在编写代码的方式上保持一致

您现在收看的是前端翻译计划,学废了的小伙伴可以订阅此专栏合集,我们每天佛系投稿,欢迎持续关注前端生态。谢谢大家的点赞,掰掰~

ref vs reactive:Vue 3 组合式 API 的“零和博弈”(译)

转载自:https://juejin.cn/post/7302698423841816616
评论
请登录