知道大家使用 Vue3 的時候有沒有這樣的疑惑,「ref、rective 都能建立一個響應式物件,我該如何選擇?」,「為什麼響應式物件解構之後就失去了響應式?應該如何處理?」 今天咱們就來全面盤點一下 ref、reactive,相信看完你一定會有不一樣的收穫,一起學起來吧!
在 Vue3 中我們可以使用 reactive()
建立一個響應式物件或陣列:
import { reactive } from 'vue'
const state = reactive({ count: 0 })
登入後複製
這個響應式物件其實就是一個 Proxy
, Vue 會在這個 Proxy
的屬性被存取時收集副作用,屬性被修改時觸發副作用。
要在元件模板中使用響應式狀態,需要在 setup()
函數中定義並返回。【相關推薦:、】
<script>
import { reactive } from 'vue'
export default { setup() { const state = reactive({ count: 0 }) return { state } }}
</script>
<template>
<div>{{ state.count }}</div>
</template>
登入後複製
當然,也可以使用 <script setup>
,<script setup>
中頂層的匯入和變數宣告可以在模板中直接使用。
<script setup>
import { reactive } from 'vue'
const state = reactive({ count: 0 })
</script>
<template>
<div>{{ state.count }}</div>
</template>
登入後複製
reactive()
返回的是一個原始物件的 Proxy
,他們是不相等的:
const raw = {}
const proxy = reactive(raw)
console.log(proxy === raw) // false
登入後複製
原始物件在模板中也是可以使用的,但修改原始物件不會觸發更新。因此,要使用 Vue 的響應式系統,就必須使用代理。
<script setup>
const state = { count: 0 }
function add() {
state.count++
}
</script>
<template>
<button @click="add">
{{ state.count }} <!-- 當點選button時,始終顯示為 0 -->
</button>
</template>
登入後複製
為保證存取代理的一致性,對同一個原始物件呼叫 reactive()
會總是返回同樣的代理物件,而對一個已存在的代理物件呼叫 reactive()
會返回其本身:
const raw = {}
const proxy1 = reactive(raw)
const proxy2 = reactive(raw)
console.log(proxy1 === proxy2) // true
console.log(reactive(proxy1) === proxy1) // true
登入後複製
這個規則對巢狀物件也適用。依靠深層響應性,響應式物件內的巢狀物件依然是代理:
const raw = {}
const proxy = reactive({ nested: raw })
const nested = reactive(raw)
console.log(proxy.nested === nested) // true
登入後複製
在 Vue 中,狀態預設都是深層響應式的。但某些場景下,我們可能想建立一個 淺層響應式物件
,讓它僅在頂層具有響應性,這時候可以使用 shallowReactive()
。
const state = shallowReactive({
foo: 1,
nested: {
bar: 2
}
})
// 狀態自身的屬性是響應式的
state.foo++
// 下層巢狀物件不是響應式的,不會按期望工作
state.nested.bar++
登入後複製
注意:淺層響應式物件應該只用於元件中的根級狀態。避免將其巢狀在深層次的響應式物件中,因為其內部的屬性具有不一致的響應行為,巢狀之後將很難理解和偵錯。
reactive() 雖然強大,但也有以下幾條限制:
僅對物件型別有效(物件、陣列和 Map
、Set
這樣的集合型別),而對 string
、number
和 boolean
這樣的原始型別無效。
因為 Vue 的響應式系統是通過屬性存取進行追蹤的,如果我們直接「替換」一個響應式物件,這會導致對初始參照的響應性連線丟失:
<script setup>
import { reactive } from 'vue'
let state = reactive({ count: 0 })
function change() { // 非響應式替換
state = reactive({ count: 1 })}
</script>
<template>
<button @click="change">
{{ state }} <!-- 當點選button時,始終顯示為 { "count": 0 } -->
</button>
</template>
登入後複製
將響應式物件的屬性賦值或解構至本地變數,或是將該屬性傳入一個函數時,會失去響應性:
const state = reactive({ count: 0 })
// n 是一個區域性變數,和 state.count 失去響應性連線
let n = state.count
// 不會影響 state
n++
// count 也和 state.count 失去了響應性連線
let { count } = state
// 不會影響 state
count++
// 引數 count 同樣和 state.count 失去了響應性連線
function callSomeFunction(count) {
// 不會影響 state
count++
}
callSomeFunction(state.count)
登入後複製
為了解決以上幾個限制,ref
閃耀登場了!
Vue 提供了一個 ref()
方法來允許我們建立使用任何值型別的響應式 ref 。
ref()
將傳入的引數包裝為一個帶有 value
屬性的 ref 物件:
import { ref } from 'vue'
const count = ref(0)
console.log(count) // { value: 0 }
count.value++
console.log(count.value) // 1
登入後複製
和響應式物件的屬性類似,ref 的 value
屬性也是響應式的。同時,當值為物件型別時,Vue 會自動使用 reactive()
處理這個值。
一個包含物件的 ref 可以響應式地替換整個物件:
<script setup>
import { ref } from 'vue'
let state = ref({ count: 0 })
function change() {
// 這是響應式替換
state.value = ref({ count: 1 })
}
</script>
<template>
<button @click="change">
{{ state }} <!-- 當點選button時,顯示為 { "count": 1 } -->
</button>
</template>
登入後複製
ref 從一般物件上解構屬性或將屬性傳遞給函數時,不會丟失響應性:
參考
const state = {
count: ref(0)
}
// 解構之後,和 state.count 依然保持響應性連線
const { count } = state
// 會影響 state
count.value++
// 該函數接收一個 ref, 和傳入的值保持響應性連線
function callSomeFunction(count) {
// 會影響 state
count.value++
}
callSomeFunction(state.count)
登入後複製
ref()
讓我們能建立使用任何值型別的 ref 物件,並能夠在不丟失響應性的前提下傳遞這些物件。這個功能非常重要,經常用於將邏輯提取到 組合式函數
中。
// mouse.js
export function useMouse() {
const x = ref(0)
const y = ref(0)
// ...
return { x, y }
}
登入後複製
<script setup>
import { useMouse } from './mouse.js'
// 可以解構而不會失去響應性
const { x, y } = useMouse()
</script>
登入後複製
所謂解包就是獲取到 ref 物件上 value
屬性的值。常用的兩種方法就是 .value
和 unref()
。 unref()
是 Vue 提供的方法,如果引數是 ref ,則返回 value 屬性的值,否則返回引數本身。
當 ref 在模板中作為頂層屬性被存取時,它們會被自動解包,不需要使用 .value
。下面是之前的例子,使用 ref()
代替:
<script setup>
import { ref } from 'vue'
const count = ref(0)
</script>
<template>
<div>
{{ count }} <!-- 無需 .value -->
</div>
</template>
登入後複製
還有一種情況,如果文字插值({{ }}
)計算的最終值是 ref
,也會被自動解包。下面的非頂層屬性會被正確渲染出來。
<script setup>
import { ref } from 'vue'
const object = { foo: ref(1) }
</script>
<template>
<div>
{{ object.foo }} <!-- 無需 .value -->
</div>
</template>
登入後複製
其他情況則不會被自動解包,如:object.foo 不是頂層屬性,文字插值({{ }}
)計算的最終值也不是 ref:
const object = { foo: ref(1) }
登入後複製
下面的內容將不會像預期的那樣工作:
<div>{{ object.foo + 1 }}</div>
登入後複製
渲染的結果會是 [object Object]1
,因為 object.foo
是一個 ref 物件。我們可以通過將 foo
改成頂層屬性來解決這個問題:
const object = { foo: ref(1) }
const { foo } = object
登入後複製
<div>{{ foo + 1 }}</div>
登入後複製
現在結果就可以正確地渲染出來了。
當一個 ref
被巢狀在一個響應式物件中,作為屬性被存取或更改時,它會自動解包,因此會表現得和一般的屬性一樣:
const count = ref(0)
const state = reactive({ count })
console.log(state.count) // 0
state.count = 1
console.log(state.count) // 1
登入後複製
只有當巢狀在一個深層響應式物件內時,才會發生解包。當 ref 作為 淺層響應式物件
的屬性被存取時則不會解包:
const count = ref(0)
const state = shallowReactive({ count })
console.log(state.count) // { value: 0 } 而不是 0
登入後複製
如果將一個新的 ref 賦值給一個已經關聯 ref 的屬性,那麼它會替換掉舊的 ref:
const count = ref(1)
const state = reactive({ count })
const otherCount = ref(2)
state.count = otherCount
console.log(state.count) // 2
// 此時 count 已經和 state.count 失去連線
console.log(count.value) // 1
登入後複製
跟響應式物件不同,當 ref 作為響應式陣列或像 Map
這種原生集合型別的元素被存取時,不會進行解包。
const books = reactive([ref('Vue 3 Guide')])
// 這裡需要 .value
console.log(books[0].value)
const map = reactive(new Map([['count', ref(0)]]))
// 這裡需要 .value
console.log(map.get('count').value)
登入後複製
toRef
是基於響應式物件上的一個屬性,建立一個對應的 ref 的方法。這樣建立的 ref 與其源屬性保持同步:改變源屬性的值將更新 ref 的值,反之亦然。
const state = reactive({
foo: 1,
bar: 2
})
const fooRef = toRef(state, 'foo')
// 更改源屬性會更新該 ref
state.foo++
console.log(fooRef.value) // 2
// 更改該 ref 也會更新源屬性
fooRef.value++
console.log(state.foo) // 3
登入後複製
toRef()
在你想把一個 prop 的 ref 傳遞給一個組合式函數時會很有用:
<script setup>
import { toRef } from 'vue'
const props = defineProps(/* ... */)
// 將 `props.foo` 轉換為 ref,然後傳入一個組合式函數
useSomeFeature(toRef(props, 'foo'))
</script>
登入後複製
當 toRef
與元件 props 結合使用時,關於禁止對 props 做出更改的限制依然有效。如果將新的值傳遞給 ref 等效於嘗試直接更改 props,這是不允許的。在這種場景下,你可以考慮使用帶有 get
和 set
的 computed
替代。
注意:即使源屬性當前不存在,toRef()
也會返回一個可用的 ref。這讓它在處理可選 props 的時候非常有用,相比之下 toRefs
就不會為可選 props 建立對應的 refs 。下面我們就來了解一下 toRefs
。
toRefs()
是將一個響應式物件上的所有屬性都轉為 ref ,然後再將這些 ref 組合為一個普通物件的方法。這個普通物件的每個屬性和源物件的屬性保持同步。
const state = reactive({
foo: 1,
bar: 2
})
// 相當於
// const stateAsRefs = {
// foo: toRef(state, 'foo'),
// bar: toRef(state, 'bar')
// }
const stateAsRefs = toRefs(state)
state.foo++
console.log(stateAsRefs.foo.value) // 2
stateAsRefs.foo.value++
console.log(state.foo) // 3
登入後複製
從組合式函數中返回響應式物件時,toRefs
相當有用。它可以使我們解構返回的物件時,不失去響應性:
// feature.js
export function useFeature() {
const state = reactive({
foo: 1,
bar: 2
})
// ...
// 返回時將屬性都轉為 ref
return toRefs(state)
}
登入後複製
<script setup>
import { useFeature } from './feature.js'
// 可以解構而不會失去響應性
const { foo, bar } = useFeature()
</script>
登入後複製
toRefs
只會為源物件上已存在的屬性建立 ref。如果要為還不存在的屬性建立 ref,就要用到上面提到的 toRef
。
以上就是 ref、reactive 的詳細用法,不知道你有沒有新的收穫。接下來,我們來探討一下響應式原理。
大家都知道 Vue2 中的響應式是採⽤ Object.defineProperty() , 通過 getter / setter 進行屬性的攔截。這種方式對舊版本瀏覽器的支援更加友好,但它有眾多缺點:
初始化時只會對已存在的物件屬性進行響應式處理。也是說新增或刪除屬性,Vue 是監聽不到的。必須使用特殊的 API 處理。
陣列是通過覆蓋原型物件上的7個⽅法進行實現。如果通過下標去修改資料,Vue 同樣是無法感知的。也要使用特殊的 API 處理。
無法處理像 Map
、 Set
這樣的集合型別。
帶有響應式狀態的邏輯不方便複用。
針對上述情況,Vue3 的響應式系統橫空出世了!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
}
登入後複製
不難看出,當將一個響應性物件的屬性解構為一個區域性變數時,響應性就會「斷開連線」。因為對區域性變數的存取不會觸發 get / set 代理捕獲。
我們回到響應式原理。在 track()
內部,我們會檢查當前是否有正在執行的副作用。如果有,就會查詢到儲存了所有追蹤了該屬性的訂閱者的 Set,然後將當前這個副作用作為新訂閱者新增到該 Set 中。
// activeEffect 會在一個副作用就要執行之前被設定
let activeEffect
function track(target, key) {
if (activeEffect) {
const effects = getSubscribersForProperty(target, key)
effects.add(activeEffect)
}
}
登入後複製
副作用訂閱將被儲存在一個全域性的 WeakMap<target, Map<key, Set<effect>>>
資料結構中。如果在第一次追蹤時沒有找到對相應屬性訂閱的副作用集合,它將會在這裡新建。這就是 getSubscribersForProperty()
函數所做的事。
在 trigger()
之中,我們會再次查詢到該屬性的所有訂閱副作用。這一次我們全部執行它們:
function trigger(target, key) {
const effects = getSubscribersForProperty(target, key)
effects.forEach((effect) => effect())
}
登入後複製
這些副作用就是用來執行 diff 演演算法,從而更新頁面的。
這就是響應式系統的大致原理,Vue3 還做了編譯器的優化,diff 演演算法的優化等等。不得不佩服尤大大,把 Vue 的響應式系統又提升了一個臺階!
(學習視訊分享:、)
以上就是全面盤點一下vue3中的ref、reactive的詳細內容,更多請關注TW511.COM其它相關文章!