記得我們在 ref
的篇章有講過「解包」嗎?
官方文件在帶過 reactive
後,又細講了它倆解包的細節⋯⋯小菜菜在學習這邊的時候遇到了幾個蠻有趣的狀況坑,來跟大家分享一下。
解包,也就是 unpacking,在程式的世界中指的是:將物件、陣列這種複合的數據中的屬性提取出來。
(可能在不同語言中不太一樣,這邊就是講解 Vue 的喔)
Vue 中的解包概念,如 JS 原生的「解構賦值」,可以將 {}
、[]
中的value
直接用簡化的方式存取到變數中。
// 宣告一個物件
const obj = { a: 1, b: 2 };
// 未解構範例
console.log(obj.a, obj.b); // 使用一般「物件.屬性」的方式,印出 1 2
// 解構範例
const { a, b } = obj; // 解構賦值
console.log(a, b); // 使用直接「存取解構後的變數」的方式,印出 1 2
(而這邊就不對原生 JS 解構技巧多作講述,大家可以到 MDN 了解)
直接來看 Vue 響應式用法 ref
、reactive
中解包的稀奇古怪狀況!
如在 ref 上篇的範例二 中測試後的結論:
<script setup> import { ref } from "vue"; const count = ref(0); </script> <template> <button @click="count++">{{ count }}</button> </template>
在 中,Vue 會自動解包 ref 的物件,所以不需要寫成 count.value,可以直接透過 count 去取值。
count++、{{ count }} 都是解包後的寫法喔!
ref()
其中包裹一個原始值,在模板中是會解包的。
如 ref 下篇,在「深層響應性中的範例」 中測試後的結論:ref()
其中包裹一個物件,在模板中是不會解包的。
以下程式碼,count、obj
為頂級屬性,在模板使用會被解包。obj.countKey
不是頂級屬性,在模板使用不會被解包。
const count = ref(0);
const obj = { countKey: ref(1) };
依循步驟試試看它們都是些什麼!
先印出兩者的 RefImpl
物件:
而如果我們在模板這樣使用:
<template>
<h3>count:{{ count }}</h3>
<h3>count+1:{{ count + 1 }}</h3>
<h3>obj.countKey:{{ obj.countKey }}</h3>
<h3>obj.countKey+1:{{ obj.countKey + 1 }}</h3>
</template>
分別會印出:
來解析一下:
{{ count }}
:為頂級屬性,會被解包。{{ count + 1 }}
:正常的反應了響應性。{{ obj.countKey }}
:非頂級屬性,不會被解包,但這邊卻有了奇怪的現象,為什麼還是印出 1
呢。這裡是官方文件提到的:
另一個需要注意的點是,如果 ref 是文本插值的最終計算值 (即 {{ }} 標籤),那麼它將被解包:
該特性僅僅是文本插值的一個便利特性,等價於 {{ object.id.value }}。
會被解包,是因為 {{ obj.countKey }}
是 ref(1)
,是文本插值的最終計算值。
{{ obj.countKey + 1 }}
:印出了 [object Object]1
。[object Object]
是 JS 中,將物件強行轉為字串型別時,會呈現的字串。obj.countKey + 1
程式碼中:obj.countKey
並非被解包的數字,而是 RefImpl
物件。我們可以透過解構,將其定義為頂級屬性:
const obj = { countKey: ref(1) };
const { countKey } = obj;
我們將其印出:
<template>
<h3>解構的 countKey:{{ countKey }}</h3>
<h3>解構的 countKey+1:{{ countKey + 1 }}</h3>
</template>
就會是預期中的結果啦:countKey
是在模板中被解包了的 ref(1)
,即 1。countKey+1
為 2。
而在這邊我們先提一下官方文件在 章節 額外的 ref 解包細節 提到的要點:
作為 reactive 對象的屬性
一個 ref 會在作為響應式對象的屬性被訪問或修改時自動解包。換句話說,它的行為就像一個普通的屬性
看英文版的比較清楚:
A ref is automatically unwrapped when accessed or mutated as a property of a reactive object. In other words, it behaves like a normal property
“as a property of a reactive object.” 也就是:當 ref
被 reactive
綁定的時候。
因此,解包的行為讓 ref
這個 RefImpl
物件「像一般的物件屬性」一樣,不用再 .value
處理內部的值。
請銘記「像一般的物件屬性」這個感受!
<script setup>
import { ref } from "vue";
import { reactive } from "vue";
const countRef = ref(0);
const reactiveObj = reactive({ count: countRef });
console.log(reactiveObj);
console.log(reactiveObj.count);
</script>
<template>
<h3>reactiveObj:{{ reactiveObj }}</h3>
<h3>用 .count 存取內部屬性值:{{ reactiveObj.count }}</h3>
</template>
讓我們一步一步看看以上做了什麼:
ref(0)
這個 RefImpl
物件存到 countRef
變數中。const countRef = ref(0);
countRef
變數定義為 count
屬性的值,並以物件包起來,當作 reactive
的參數,再存進 reactiveObj
變數。const reactiveObj = reactive({ count: countRef });
reactiveObj
和 reactiveObj.count
的結果console.log(reactiveObj);
console.log(reactiveObj.count);
reactiveObj
和 reactiveObj.count
的值<template>
<h3>reactiveObj:{{ reactiveObj }}</h3>
<h3>用 .count 存取內部屬性值:{{ reactiveObj.count }}</h3>
</template>
瀏覽器上會呈現什麼呢?
右邊 console.log
:
console.log(reactiveObj);
:reactiveObj
為我們用 reactive
綁定的 { count: countRef }
,是一個 Proxy
物件。console.log(reactiveObj.count);
:這裡發生了解包。reactiveObj
用 .count
存取屬性的方式,直接存取到了其中 ref
物件內的 _value
。左邊畫面:
reactiveObj
為我們綁定的 { count: countRef }
物件。countRef
自動解包了 ref(0)
,因此呈現 { "count": 0 }
。reactiveObj.count
:這裡發生了解包。reactiveObj
用 .count
存取屬性的方式,直接存取到了其中 ref
物件內的 _value
。因此,當 ref
成為了 reactive()
物件中的「屬性」時,會讓 ref
物件在 reactive
中可以「像一般的物件屬性」被存取。
ref
若以「陣列型態」存在 reactive
中的話不會解包。
官方文件:與 reactive 對象不同的是,當 ref 作為響應式數組或原生集合類型 (如 Map) 中的元素被訪問時,它不會被解包:
const books = reactive([ref('Vue 3 Guide')]) // 這裡需要 .value console.log(books[0].value)
我們可以試試看用 reactive
包一個 ref
的陣列。
const classmates = reactive([ref("Jami"), ref("Irene"), ref("Jenny")]);
console.log(classmates);
印出來看~reactive
物件中有一個陣列,陣列其中有三個 RefImpl
物件:
console.log(classmates[0]);
我們取得第一個陣列,印出來看看:
這情況不會被解包。
(和 ref
以「物件」形式傳入 reactive
的時候的解包情況不同)
因此我若以 _value
語法,就可以取到內部的 Jami 囉。
console.log(classmates[0].value);
另外我們試試看用 reactive
包一個 ref
的 Map。
const classmateMap = new Map([["name", ref("Jami")]])
console.log(classmateMap.get(name))
印出來是一個 RefImpl 物件,並未解包:
需要用 _value
方式取得值:
console.log(classmateMap.get("name").value);
這一小節是我自己在實驗的時候,發現的情況,問問了我的前端捧友⋯⋯他說實務上很少遇到,但我就也是確實在學習的過程中踩到了 QQ 陪我研究一下吧(在跟誰講話)
如果我的 ref
物件不包起來當作 reactive
的屬性,而是「直接傳入」呢?
我們實作一下範例,把 ref
分別用兩種形式傳給 reactive
:
<script setup>
import { ref } from "vue";
import { reactive } from "vue";
const count = ref(0);
const reactiveObj = reactive({ count }); // 作為物件傳入
const reactiveObj2 = reactive(count); // 直接傳入
console.log(`reactiveObj`, reactiveObj);
console.log(`reactiveObj.count`, reactiveObj.count);
console.log(`reactiveObj2`, reactiveObj2);
console.log(`reactiveObj2.value`, reactiveObj2.value);
</script>
瀏覽器上結果:
reactiveObj
是 ref
作為「物件」傳入 reactive
的方式得出的。reactiveObj2
:是 ref
「直接」傳入 reactive
的方式得出的。來看看都印出了什麼東東:
reactiveObj
:結果為一個 Proxy
物件,內部有屬性 count
,值為 ref(0)
這個 RefImpl
物件。reactiveObj.count
:以 .count
取 reactiveObj
的值,為 0
(其中 ref(0)
發生解包,所以不用再 .value
)。reactiveObj2
:結果為一個 Proxy
物件,內部直接是 ref(0)
這個 RefImpl
物件。reactiveObj2.value
:由於內部是一個 RefImpl
物件,並非 Proxy
響應式物件,需用 _value
取值,為 0
(不會被解包)。我們加上這段:
<template>
<h3>reactiveObj.count >> {{ reactiveObj.count }}</h3>
<h3>reactiveObj2.value >> {{ reactiveObj2.value }}</h3>
<h3>reactiveObj2 >> {{ reactiveObj2 }}</h3>
</template>
再到瀏覽器上看看:
1.reactiveObj.count
:以 .count
取 reactiveObj
的值,為 0
(其中 ref(0)
發生解包,所以不用再 .value
),這沒問題。
再來是不是發現了什麼詭異的情況?
reactiveObj2.value
:reactiveObj2
是 ref
「直接」傳入 reactive
得出的。console.log
就是這樣取值,為什麼到這邊卻沒有印出東西?reactiveObj2
又印出 0
?停下來,思考一下這幾點:
ref(0)
在模板會自動解包,直接變成 0
。reactive()
只會接受「物件」屬性作為參數。因此 Vue 會嘗試將解包後的 0
像 reactive(0)
這樣處理。
重新看一下程式碼:
<script setup>
const count = ref(0);
const reactiveObj2 = reactive(count); // 直接傳入
</script>
<template>
<h3>reactiveObj2.value >> {{ reactiveObj2.value }}</h3>
<h3>reactiveObj2 >> {{ reactiveObj2 }}</h3>
</template>
回答問題!
reactiveObj2.value
:reactiveObj2
是 ref
「直接」傳入 reactive
得出的。console.log
就是這樣取值,為什麼到這邊卻沒有印出東西?reactive()
只會接受「物件」屬性作為參數,而 0
並不是一個物件,它沒有 .value
屬性,因此顯示不出來。reactiveObj2
又印出 0
?reactive()
只會接受「物件」屬性作為參數,但如果傳入的是一個非物件類型,還是會正常呈現,這邊我想到的是上面說的 ref
若是文本插值的最終值的話,就會被解包。這暫時還沒有找到文件論點支持這個想法
目前的想法是覺得:Vue 沒有報錯,是反映了ref(0)
解包後的真實情況。
實際上就是?
<template>
<h3>reactive(0) >> {{ reactive(0) }}</h3>
</template>
整理為一個表格,不然好像有點累:
情境 | 解包行為 |
---|---|
模板中的 ref 為原始值 | 會解包 |
模板中的 ref 為物件值 | 不解包 |
模板中的 ref 為頂級屬性 | 會解包 |
ref 以 {} 物件型態,作為 reactive 參數 | 會解包 |
ref 以陣列型態,作為 reactive 參數 | 不解包 |
ref(RefImpl 物件)直接作為 reactive 參數 | 不解包 |
感謝看到這裡的你⋯⋯相信到這邊大家是否也鬆一口氣了。
深呼吸一下⋯⋯
響應式基礎的內容我們到這邊告一個段落,為大家附上小目錄 標題也太長了吧:
這篇寫到有點超出預期的發展(字數和資訊量都是 XD),也有點不太確定自己吸收的和實證的是否正確,或是有更完善的思考方式和範例,歡迎大家提點!
鐵人賽經過 1/2 啦~~~~~~~
https://github.com/Jamixcs/2024iThome-jamixcs/tree/main/src/components/day15