乐趣区

关于vue3:译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 的不同:

reactive ref
👎只对 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 获取更多内容,同时能够及时接管新的文章推送。

退出移动版