【Vuejs】使用 Ref 還是 Reactive?
我喜歡 Vue 3 的 Composition API,它提供了兩種方法來爲 Vue 組件添加響應式狀態:ref
和reactive
。當你使用ref
時到處使用.value
是很麻煩的,但當你用reactive
創建的響應式對象進行重構時,也很容易丟失響應性。 在這篇文章中,我將闡釋你如何來選擇reactive
以及ref
。
一句話總結:默認情況下使用
ref
,當你需要對變量分組時使用reactive
。
Vue3 的響應式
在我解釋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 響應式系統的更多細節。
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 有兩個限制:
第一個限制是,它只適用於對象類型,比如對象、數組和集合類型,如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)
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] 擴展來自動地添加.value
到ref
上。你可以在Volar: Auto Complete Refs
設置中開啓:
相應的 JSON 設置:
"volar.autoCompleteRefs": true
爲了減少 CPU 的使用,這個功能默認是禁用的。
比較
讓我們總結一下reactive
和ref
之間的區別:
我的觀點
我最喜歡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
,也完全沒有問題。
ref
和reactive
都是在 Vue 3 中創建響應式變量的強大工具。你甚至可以在沒有任何技術缺陷的情況下同時使用它們。只要你選擇你喜歡的那一個,並儘量在寫代碼時保持一致就可以了!
- 原文鏈接:https://mokkapps.de/blog/ref-vs-reactive-what-to-choose-using-vue-3-composition-api[5]
參考資料
[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