【Vuejs】使用 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()的實現 [1]。

不幸的是,也不能對用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()[2] 是一個便捷的實用函數,在你的值可能是一個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[3] 擴展來自動地添加.valueref上。你可以在Volar: Auto Complete Refs設置中開啓:

相應的 JSON 設置:

"volar.autoCompleteRefs": true

爲了減少 CPU 的使用,這個功能默認是禁用的。

比較

讓我們總結一下reactiveref之間的區別:

8UdbmY

我的觀點

我最喜歡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[4] 這樣的庫中,他們使用reactive()來設置驗證的狀態。

總結起來,社區中的最佳實踐是默認使用ref,在需要分組的時候使用reactive

總結

那麼,你究竟該使用ref還是reactive

我的建議是默認使用ref,當你需要分組時使用reactive。Vue 社區也有同樣的觀點,但如果你決定默認使用reactive,也完全沒有問題。

refreactive都是在 Vue 3 中創建響應式變量的強大工具。你甚至可以在沒有任何技術缺陷的情況下同時使用它們。只要你選擇你喜歡的那一個,並儘量在寫代碼時保持一致就可以了!

參考資料

[1] 實現: https://github.com/vuejs/core/blob/main/packages/reactivity/src/ref.ts

[2] unref(): https://vuejs.org/api/reactivity-utilities.html#unref

[3] Volar: https://marketplace.visualstudio.com/items?itemName=Vue.volar

[4] Vuelidate: https://vuelidate.js.org/

[5] https://mokkapps.de/blog/ref-vs-reactive-what-to-choose-using-vue-3-composition-api: https://mokkapps.de/blog/ref-vs-reactive-what-to-choose-using-vue-3-composition-api

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/MIHFR471-TWMXOJXm8tbGQ