使用Ref還是Reactive?

2023-04-25 06:02:04

我喜歡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設定中開啟:

相應的JSON設定:

"volar.autoCompleteRefs": true

為了減少CPU的使用,這個功能預設是禁用的。

比較

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

reactive ref