我喜歡Vue 3的Composition API,它提供了兩種方法來為Vue元件新增響應式狀態:ref
和reactive
。當你使用ref
時到處使用.value
是很麻煩的,但當你用reactive
建立的響應式物件進行重構時,也很容易丟失響應性。 在這篇文章中,我將闡釋你如何來選擇reactive
以及ref
。
一句話總結:預設情況下使用
ref
,當你需要對變數分組時使用reactive
。
在我解釋ref
和reactive
之前,你應該瞭解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)
},
})
}
proxy
的get
和set
方法通常被稱為代理陷阱。
這裡強烈建議閱讀官方檔案來檢視有關Vue響應式系統的更多細節。
現在,讓我們來分析下,你如何使用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有兩個限制:
第一個限制是,它只適用於物件型別,比如物件、陣列和集合型別,如Map
和Set
。它不適用於原始型別,比如string
、number
或boolean
。
第二個限制是,從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)
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',
}
在使用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擴充套件來自動地新增.value
到ref
上。你可以在Volar: Auto Complete Refs
設定中開啟:
相應的JSON設定:
"volar.autoCompleteRefs": true
為了減少CPU的使用,這個功能預設是禁用的。
讓我們總結一下reactive
和ref
之間的區別:
reactive | ref |
---|---|
|