iT邦幫忙

2024 iThome 鐵人賽

DAY 14
1
Modern Web

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

欸你是要進 Vue 了沒? - Day14:Vue 你怎麼 DOM 起來了?響應式用法就還有另外一種叫 reactive

  • 分享至 

  • xImage
  •  

https://ithelp.ithome.com.tw/upload/images/20240928/201691390vsKzdnkae.jpg

怎麼是梗圖開場 XD (來源:網路)

前幾天我們介紹了 Vue 中響應式的基礎 ref 以及 Proxy 的機制,今天直接來看另一種用法 reactive

由於篇幅較長,先列出摘要:

  • 定義
  • 語法
    reactive 和 ref 不同之處
  • reactive 特點
    Proxy 物件並不等於原始物件
    只有 Proxy 物件具響應性,原始物件不具備
    Proxy 物件怎麼確保響應性?
  • reactive 的局限性
    不能夠綁定原始值
    不能隨意更動綁定的原始物件
    解構後會丟失響應性
  • 官方建議

我們開始囉!
/images/emoticon/emoticon27.gif

定義

官方文件:還有另一種聲明響應式狀態的方式,即使用 reactive() API。與將內部值包裝在特殊對象中的 ref 不同,reactive() 將使對象本身具有響應性

響應式對象是 JavaScript 代理,其行為就和普通對象一樣。不同的是,Vue 能夠攔截對響應式對象所有屬性的訪問和修改,以便進行依賴追蹤和觸發更新。

語法

reactive();

括號內可定義「物件值」,也就是你要請 reactive 追蹤的參數。

小道消息

我們先悄悄說一下 reactive 這個方式的綁定限制,之後會以範例帶大家來看看:

官方文件:它只能用於對象類型 (對象、數組和如 Map、Set 這樣的集合類型)。它不能持有如 string、number 或 boolean 這樣的原始類型。

不過,之前有提到 ref 也能夠綁定物件,那為什麼還需要 reactive 呢?

reactive 和 ref 不同之處

  • reactive 是直接透過 Proxy 物件的概念實現的,會讓物件本身具有響應性。
  • ref 則是將自己包裝成 RefImfp 物件去追蹤 _value 屬性的變更。

我們可以分別用它倆包一個物件來看看區別!

範例

<script setup>
import { reactive } from "vue";
import { ref } from "vue";

const obj = { today: "今天是鐵人賽第 14 天,你還活著嗎" };
const reactiveObj = reactive(obj);
const refObj = ref(obj);
console.log(reactiveObj);
console.log(refObj);
</script>

<template>
  <h3>reactive example: {{ reactiveObj.today }}</h3>
  <h3>ref example: {{ refObj.today }}</h3>
</template>

以上做了什麼:

  1. 定義了一個 obj 物件,其中包含屬性 today,並用 reactiveref 兩種方式進行響應性綁定。
  2. reactiveObj 是通過 reactiveobj 轉換為一個 Proxy 物件,會使整個物件都具有響應性。
  3. refObj 是通過 refobj 包裝起來的,是一個 RefImpl 物件。

瀏覽器上可以看到:

  1. reactiveObj:它是一個 Proxy 物件,內部有 today 屬性。

我們複習一下昨天講到的 Proxy 概念:它是一個代理物件,內部會有攔截機制(get、set 等等方法)。
因此我們在模板使用 <h3>reactive example: {{ reactiveObj.today }}</h3> 時,會觸發內部 Proxyget 機制,因此可取到 today 的值。

  1. refObj:是一個 RefImpl 物件,用 _value 值去追蹤內部的值,而 _value 內有一個 Proxy 物件。

我們複習一下 ref 在模板中解包的 概念:

  • RefImpl 物件 在模板中會自動解包,不用透過 _value 拿到值。(複習看這裡
  • 但若綁定的是物件,不會解包,但可以透過 .屬性 取得值。(複習看這裡

所以在這邊我們使用 <h3>ref example: {{ refObj.today }}</h3> 時:

  • refObj 這個 RefImpl 物件會自動解包(不用透過 _value 拿到 obj
  • obj 是一個物件,所以我們透過 .today 再取得值。

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

reactive 的特點

  1. Proxy 物件並不等於原始物件
  2. 只有 Proxy 物件具響應性,原始物件不具備

Proxy 物件並不等於原始物件

我們可以從中觀察:

<script setup>
import { reactive } from "vue";

const obj = { today: "今天是鐵人賽第 14 天,你還活著嗎" };
const reactiveObj = reactive(obj);
console.log(`Proxy 物件是否等於原始物件:`, obj === reactiveObj);
</script>

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

答案是 false

只有 Proxy 物件具響應性,原始物件不具備

官方文件:只有代理對象是響應式的,更改原始對象不會觸發更新。因此,使用 Vue 的響應式系統的最佳實踐是僅使用你聲明對象的代理版本。

我們可以這樣試試看去改原始物件、代理物件:

<script setup>
import { reactive } from "vue";

const obj = { answer: "" };
const reactiveObj = reactive(obj);

// 修改原始物件
function changeOriginal() {
  obj.answer = "原始物件鼠掉了";
  console.log(obj);
}
// 修改代理物件
function changeReactive() {
  reactiveObj.answer = "代理物件鼠掉了";
  console.log(reactiveObj);
}
</script>

<template>
  <div class="container">
    <h1>今天是鐵人賽第 14 天</h1>
    <p>點擊下面的按鈕來看看原始物件和代理物件的不同反應。</p>
    <div>
      <h3>· 原始物件 你還活著嗎? {{ obj.answer }}</h3>
      <button @click="changeOriginal">原始物件要回答</button>
    </div>
    <div>
      <h3>· 代理物件 你還活著嗎? {{ reactiveObj.answer }}</h3>
      <button @click="changeReactive">代理物件要回答</button>
    </div>
  </div>
</template>

我們用 changeOriginal()changeReactive() 兩個 function 分別去對 objreactiveObj 的值做修改並將目前原始物件印出。

  • changeOriginal 不會觸發 UI 更新
  • changeReactive 使用 reactiveObj 響應式物件修改的值會觸發 reactive(obj) 進而觸發 UI 更新。

瀏覽器上可看見:
兩個按鈕點擊後 obj 物件中的值是有被更改的,唯獨差異是:直接用「原始物件」方式改的,「不會觸發」 UI 更新。

這邊也要提到為什麼用 reactiveObj.answer = "代理物件鼠掉了"; 這個方法改值,{{ obj.answer }} 的 UI 也會更新?
因為原始物件和代理物件是指向同一個記憶體的 因為 Proxy 物件是對原始物件的代理,當代理物件修改屬性時,這些更改會「直接反映到原始物件」上,所以修改代理物件的屬性會同步影響原始物件的屬性。
image

這是一則插播小小工商

鼠掉了沒有關係,還是鼠出了電光一閃
(import 橘子の DOM Event 系列文 - 元素不可思議事件簿!很有趣 XD

Proxy 物件怎麼確保響應性?

官方文件:為保證訪問代理的一致性,對同一個原始對象調用 reactive() 會總是返回同樣的代理對象,而對一個已存在的代理對象調用 reactive() 會返回其本身

翻譯蒟蒻:所以說只要使用了 reactive() 來綁定物件,它的原始物件和代理物件都會返回同一個代理物件。

我們可以看看這個例子:

<script setup>
import { reactive } from "vue";

const rawObj = {};
const proxyObj = reactive(rawObj);
const proxyObj2 = reactive(proxyObj);

console.log(
  `reactive(rawObj) 是否等於 proxyObj:`,
  reactive(rawObj) === proxyObj
);
console.log(
  `reactive(proxyObj) 是否等於 proxyObj:`,
  reactive(proxyObj) === proxyObj
);
console.log(
  `reactive(proxyObj2) 是否等於 proxyObj:`,
  reactive(proxyObj2) === proxyObj
);
</script>

reactive(rawObj) === proxyObj:綁定了原始物件 rawObj,return 的是代理物件 proxyObj
reactive(proxyObj) === proxyObj:綁定了第一次定義的代理物件 proxyObj,return 的是它自己。
reactive(proxyObj2) === proxyObj:綁定了第一次定義的代理物件的代理物件 proxyObj2,return 的還是第一次定義的代理物件 proxyObj

無論重複綁定多少次,還是會得到「同一個」代理物件:因此確保了響應式數據被正確控管。

reactive 的局限性

  1. 不能夠綁定原始值。
  2. 不能隨意更動綁定的原始物件。
  3. 解構後會丟失響應性。

不能夠綁定原始值

當我們嘗試著綁定「數字」或「字串」時:

<script setup>
import { reactive } from "vue";

const reactiveObj = reactive(123);
const reactiveObj2 = reactive("abc");

console.log(reactiveObj);
console.log(reactiveObj2);
</script>

<template>
  <h3>{{ reactiveObj }}</h3>
  <h3>{{ reactiveObj2 }}</h3>
</template>

瀏覽器上會看到:
專案看起來並不會報錯,定義的值會正常渲染耶,但是出現了一些警告:
https://ithelp.ithome.com.tw/upload/images/20240928/20169139e4V1pzYrny.png
https://ithelp.ithome.com.tw/upload/images/20240928/20169139x1kzsDoqLW.png

那我們修改值呢?

<script setup>
import { reactive } from "vue";

const reactiveObj = reactive(123);
const reactiveObj2 = reactive("abc");

console.log(reactiveObj);
console.log(reactiveObj + 1);
console.log(reactiveObj2);
console.log(reactiveObj2 + "edf");
</script>

<template>
  <h3>{{ reactiveObj }}</h3>
  <h3>{{ reactiveObj2 }}</h3>
</template>

<template> 中的文本插值並沒有更新到最新的值!

https://ithelp.ithome.com.tw/upload/images/20240928/201691397CP0QHXUs1.png

不能隨意更動綁定的原始物件

官方文件:由於 Vue 的響應式跟蹤是通過屬性訪問實現的,因此我們必須始終保持對響應式對象的相同引用。這意味著我們不能輕易地“替換”響應式對象,因為這樣的話與第一個引用的響應性連接將丟失:

let state = reactive({ count: 0 })

// 上面的 ({ count: 0 }) 引用將不再被追蹤
// (響應性連接已丟失!)
state = reactive({ count: 1 })

我們來寫寫範例:

<script setup>
import { reactive } from "vue";

let state = reactive({ count: 0 });

function changeState() {
  state = reactive({ count: 1 }); // 替換物件,這樣會丟失響應性
}
function changeValue() {
  state.count++; // 修改屬性
}
</script>

<template>
  <div>
    <h3>替換物件範例</h3>
    <p>目前值:{{ state.count }}</p>
    <button @click="changeState">替換 state</button>

    <h3>修改屬性範例</h3>
    <p>目前值:{{ state.count }}</p>
    <button @click="changeValue">增加計數</button>
  </div>
</template>
  1. 定義了一個由 reactive() 追蹤的物件 state
  2. changeState 函式:
    「直接替換」了原始物件,從 { count: 0 } 改為 { count: 1 }
  3. changeValue 函式:
    以「修改屬性」的方式 state.count++; 增加響應式物件屬性的值。
  4. 兩個按鈕分別綁定 2. 和 3. 的函式,並且分別渲染代理物件的值。
    image

可以看到:

  1. 按下「增加計數」的按鈕後,count 的值會正常增加,介面會隨之更新。
  2. 點選「替換 state」的按鈕後,state 物件被全新的物件 { count: 1 } 替換,雖然可看到 count 的值已經被設為 1,介面卻不會更新,因為替換後導致 state 失去原本的響應性。
  3. 後續再點選增加計數,將不再更新介面。

解構後會丟失響應性

<script setup>
import { reactive } from "vue";

const reactiveObj = reactive({ count: 0 });

let { count } = reactiveObj;
count++;

console.log(`reactiveObj`, reactiveObj);
console.log(`count`, count);
</script>

<template>
  <h3>已經斷開響應性連接的:{{ reactiveObj.count }}</h3>
  <h3>被解構的變數:{{ count }}</h3>
</template>

來看看這段程式碼做了什麼:

  1. const reactiveObj = reactive({ count: 0 });:使用 reactive(){ count: 0 } 物件用響應式綁定為 reactiveObj 變數。
  2. let { count } = reactiveObj;:解構代理物件中的屬性為 count 變數。
  3. 將解構出來的 count 變數++。
  4. 印出此時的代理物件 reactiveObj 與解構後的變數 count
  5. <template> 中看兩者是否具備響應性。

瀏覽器上會看到:
https://ithelp.ithome.com.tw/upload/images/20240928/20169139oxqXex8uZm.png

右邊:

  1. reactiveObj 解構後,其內的 count 屬性 _value 還是 0,並非 1
  2. 而從其中解構後的變數 count 值為 ++ 過的 1。

左邊我們使用:

<template>
  <h3>已經斷開響應性連接的:{{ reactiveObj.count }}</h3>
  <h3>被解構的變數:{{ count }}</h3>
</template>

觀察兩者是否具備響應性,得出 reactiveObj 被解構後其屬性值 count,不具備響應性,所以還是 0。
count 則為執行 count++ 後的值 1。

官方建議?

官方文件:由於這些限制,我們建議使用 ref() 作為聲明響應式狀態的主要 API。

不過作者 尤雨溪 有在 這個影片 中談過:每個人使用的時候,多少帶點主觀成份,但只要你覺得它對是更適合你的想法,就不會有太大問題。主要還是看個人偏好和團隊共識。

小結

覺得 reactive 它的細節和要注意的地方更多(腦袋爆炸中)
而兩者的區別和更進階的應用,應該之後實作的時候就會比較有感(應該吧 嗎)
/images/emoticon/emoticon50.gif

範例 code ⬇️

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

參考資料


上一篇
欸你是要進 Vue 了沒? - Day13:Vue 你怎麼 DOM 起來了?都學到這了依舊沒辦法繞過的 JS 原生 Proxy 概念
下一篇
欸你是要進 Vue 了沒? - Day15:Vue 你怎麼 DOM 起來了?乂稀奇古怪的 ref && reactive 解包合體技乂
系列文
欸你是要進 Vue 了沒?30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

1 則留言

1
jeremykuo
iT邦新手 5 級 ‧ 2024-09-28 09:05:58

好像感受到Chris說的:「前方高能」是什麼要意思了…

++ iT邦新手 5 級 ‧ 2024-09-28 11:09:32 檢舉

/images/emoticon/emoticon36.gif

Chris iT邦新手 3 級 ‧ 2024-10-26 23:15:24 檢舉

Proxy = 鋼鐵衣
資料 = 東尼史塔克

我要留言

立即登入留言