iT邦幫忙

2024 iThome 鐵人賽

DAY 15
1
Modern Web

欸你是要進 Vue 了沒?系列 第 15

欸你是要進 Vue 了沒? - Day15:Vue 你怎麼 DOM 起來了?乂稀奇古怪的 ref && reactive 解包合體技乂

  • 分享至 

  • xImage
  •  

記得我們在 ref 的篇章有講過「解包」嗎?
官方文件在帶過 reactive 後,又細講了它倆解包的細節⋯⋯小菜菜在學習這邊的時候遇到了幾個蠻有趣的狀況,來跟大家分享一下。
/images/emoticon/emoticon68.gif

解包定義

解包,也就是 unpacking,在程式的世界中指的是:將物件、陣列這種複合的數據中的屬性提取出來。

(可能在不同語言中不太一樣,這邊就是講解 Vue 的喔)
Vue 中的解包概念,如 JS 原生的「解構賦值」,可以將 {}[] 中的value 直接用簡化的方式存取到變數中。

JS 解構賦值範例

// 宣告一個物件
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 響應式用法 refreactive 中解包的稀奇古怪狀況!

ref 一般情況的解包

模板中的 ref 原始值解包

如在 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 下篇,在「深層響應性中的範例」 中測試後的結論:
ref() 其中包裹一個物件,在模板中是不會解包的。

模板中的 ref 頂級屬性才解包

以下程式碼,
count、obj 為頂級屬性,在模板使用會被解包。
obj.countKey 不是頂級屬性,在模板使用不會被解包。

const count = ref(0);
const obj = { countKey: ref(1) };

依循步驟試試看它們都是些什麼!
先印出兩者的 RefImpl 物件:

https://ithelp.ithome.com.tw/upload/images/20240928/20169139o5Ja12yZj0.png

而如果我們在模板這樣使用:

<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>

分別會印出:
https://ithelp.ithome.com.tw/upload/images/20240928/20169139frDZ8rGdZA.png

來解析一下:

  1. {{ count }}:為頂級屬性,會被解包。
  2. {{ count + 1 }}:正常的反應了響應性。
  3. {{ obj.countKey }}:非頂級屬性,不會被解包,但這邊卻有了奇怪的現象,為什麼還是印出 1 呢。

這裡是官方文件提到的:

另一個需要注意的點是,如果 ref 是文本插值的最終計算值 (即 {{ }} 標籤),那麼它將被解包:
該特性僅僅是文本插值的一個便利特性,等價於 {{ object.id.value }}。

會被解包,是因為 {{ obj.countKey }}ref(1),是文本插值的最終計算值。

  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。
https://ithelp.ithome.com.tw/upload/images/20240928/20169139Buj3N2v8Ik.png

ref 在 reactive 中的解包

ref 為物件

而在這邊我們先提一下官方文件在 章節 額外的 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.” 也就是:當 refreactive 綁定的時候。

「它的行為就像一個普通的屬性」

因此,解包的行為讓 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>

讓我們一步一步看看以上做了什麼:

  1. ref(0) 這個 RefImpl 物件存到 countRef 變數中。
const countRef = ref(0);
  1. countRef 變數定義為 count 屬性的值,並以物件包起來,當作 reactive 的參數,再存進 reactiveObj 變數。
const reactiveObj = reactive({ count: countRef });
  1. 分別印出 reactiveObjreactiveObj.count 的結果
console.log(reactiveObj);
console.log(reactiveObj.count);
  1. 在模板中分別展示 reactiveObjreactiveObj.count 的值
<template>
  <h3>reactiveObj:{{ reactiveObj }}</h3>
  <h3>用 .count 存取內部屬性值:{{ reactiveObj.count }}</h3>
</template>

瀏覽器上會呈現什麼呢?
https://ithelp.ithome.com.tw/upload/images/20240928/20169139nUFjthlqGV.png

右邊 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 中不解包的情況

ref 為陣列型態

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 物件:
https://ithelp.ithome.com.tw/upload/images/20240928/201691393qxdl980zf.png

console.log(classmates[0]);

我們取得第一個陣列,印出來看看:
https://ithelp.ithome.com.tw/upload/images/20240928/20169139rX7XxAbU4j.png

這情況不會被解包。
(和 ref 以「物件」形式傳入 reactive 的時候的解包情況不同)

因此我若以 _value 語法,就可以取到內部的 Jami 囉。

console.log(classmates[0].value);

https://ithelp.ithome.com.tw/upload/images/20240928/20169139bfjsxFbBQh.png

另外我們試試看用 reactive 包一個 ref 的 Map。

const classmateMap = new Map([["name", ref("Jami")]])
console.log(classmateMap.get(name))

印出來是一個 RefImpl 物件,並未解包:
https://ithelp.ithome.com.tw/upload/images/20240928/20169139YEdiahP1zR.png

需要用 _value 方式取得值:

console.log(classmateMap.get("name").value);

https://ithelp.ithome.com.tw/upload/images/20240928/20169139Uc9sPKO9jc.png

ref 直接傳入 reactive

這一小節是我自己在實驗的時候,發現的情況,問問了我的前端捧友⋯⋯他說實務上很少遇到,但我就也是確實在學習的過程中踩到了 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>

瀏覽器上結果:
https://ithelp.ithome.com.tw/upload/images/20240928/20169139Jx2OJ3iT7B.png

  • reactiveObjref 作為「物件」傳入 reactive 的方式得出的。
  • reactiveObj2:是 ref 「直接」傳入 reactive 的方式得出的。

來看看都印出了什麼東東:

  1. reactiveObj:結果為一個 Proxy 物件,內部有屬性 count,值為 ref(0) 這個 RefImpl 物件。
  2. reactiveObj.count:以 .countreactiveObj 的值,為 0(其中 ref(0) 發生解包,所以不用再 .value )。
  3. reactiveObj2:結果為一個 Proxy 物件,內部直接是 ref(0) 這個 RefImpl 物件。
  4. reactiveObj2.value:由於內部是一個 RefImpl 物件,並非 Proxy 響應式物件,需用 _value 取值,為 0(不會被解包)。

模板中的行為

我們加上這段:

<template>
 <h3>reactiveObj.count >> {{ reactiveObj.count }}</h3>
  <h3>reactiveObj2.value >> {{ reactiveObj2.value }}</h3>
  <h3>reactiveObj2 >> {{ reactiveObj2 }}</h3>
</template>

再到瀏覽器上看看:
https://ithelp.ithome.com.tw/upload/images/20240928/20169139IexZhlqOOs.png
1.reactiveObj.count:以 .countreactiveObj 的值,為 0(其中 ref(0) 發生解包,所以不用再 .value ),這沒問題。

再來是不是發現了什麼詭異的情況?

  1. reactiveObj2.valuereactiveObj2ref 「直接」傳入 reactive 得出的。
    剛剛 console.log 就是這樣取值,為什麼到這邊卻沒有印出東西?
  2. reactiveObj2 又印出 0

停下來,思考一下這幾點:

  • ref(0) 在模板會自動解包,直接變成 0
  • reactive() 只會接受「物件」屬性作為參數。

因此 Vue 會嘗試將解包後的 0reactive(0) 這樣處理。

重新看一下程式碼:

<script setup>
const count = ref(0);
const reactiveObj2 = reactive(count); // 直接傳入
</script>

<template>
  <h3>reactiveObj2.value >> {{ reactiveObj2.value }}</h3>
  <h3>reactiveObj2 >> {{ reactiveObj2 }}</h3>
</template>

回答問題!

  1. reactiveObj2.valuereactiveObj2ref 「直接」傳入 reactive 得出的。
    剛剛 console.log 就是這樣取值,為什麼到這邊卻沒有印出東西?
    reactive() 只會接受「物件」屬性作為參數,而 0 並不是一個物件,它沒有 .value 屬性,因此顯示不出來。
  2. reactiveObj2 又印出 0
    雖然說 reactive() 只會接受「物件」屬性作為參數,但如果傳入的是一個非物件類型,還是會正常呈現,這邊我想到的是上面說的 ref 若是文本插值的最終值的話,就會被解包。

這暫時還沒有找到文件論點支持這個想法
目前的想法是覺得:Vue 沒有報錯,是反映了 ref(0) 解包後的真實情況。

實際上就是?

<template>
  <h3>reactive(0) >> {{ reactive(0) }}</h3>
</template>

https://ithelp.ithome.com.tw/upload/images/20240928/20169139PNgvDaFX2D.png

大結

整理為一個表格,不然好像有點累:

情境 解包行為
模板中的 ref 為原始值 會解包
模板中的 ref 為物件值 不解包
模板中的 ref 為頂級屬性 會解包
ref 以 {} 物件型態,作為 reactive 參數 會解包
ref 以陣列型態,作為 reactive 參數 不解包
ref(RefImpl 物件)直接作為 reactive 參數 不解包

小結

感謝看到這裡的你⋯⋯相信到這邊大家是否也鬆一口氣了。
深呼吸一下⋯⋯

響應式基礎的內容我們到這邊告一個段落,為大家附上小目錄 標題也太長了吧

這篇寫到有點超出預期的發展(字數和資訊量都是 XD),也有點不太確定自己吸收的和實證的是否正確,或是有更完善的思考方式和範例,歡迎大家提點!

鐵人賽經過 1/2 啦~~~~~~~
/images/emoticon/emoticon64.gif

範例 code ⬇️

https://github.com/Jamixcs/2024iThome-jamixcs/tree/main/src/components/day15

參考資料


上一篇
欸你是要進 Vue 了沒? - Day14:Vue 你怎麼 DOM 起來了?響應式用法就還有另外一種叫 reactive
下一篇
欸你是要進 Vue 了沒? - Day16:Vue 的 computed 在算什麼東西
系列文
欸你是要進 Vue 了沒?23
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

1 則留言

1
jeremykuo
iT邦新手 5 級 ‧ 2024-09-29 10:26:18

這篇太補了(流鼻血),甚至還出動了大傑image

++ iT邦新手 5 級 ‧ 2024-09-29 10:46:53 檢舉

https://ithelp.ithome.com.tw/upload/images/20240929/20169139VDkqbCS4sb.jpg

橘子 iT邦新手 5 級 ‧ 2024-09-29 14:12:28 檢舉

小傑利鼠

++ iT邦新手 5 級 ‧ 2024-09-29 15:10:15 檢舉

hi 橘https://ithelp.ithome.com.tw/upload/images/20240929/20169139gy4CngHy0i.jpg

我要留言

立即登入留言