怎麼是梗圖開場 XD (來源:網路)
前幾天我們介紹了 Vue 中響應式的基礎 ref
以及 Proxy
的機制,今天直接來看另一種用法 reactive
!
由於篇幅較長,先列出摘要:
- 定義
- 語法
reactive 和 ref 不同之處- reactive 特點
Proxy 物件並不等於原始物件
只有 Proxy 物件具響應性,原始物件不具備
Proxy 物件怎麼確保響應性?- reactive 的局限性
不能夠綁定原始值
不能隨意更動綁定的原始物件
解構後會丟失響應性- 官方建議
我們開始囉!
官方文件:還有另一種聲明響應式狀態的方式,即使用 reactive() API。與將內部值包裝在特殊對象中的 ref 不同,reactive() 將使對象本身具有響應性
響應式對象是 JavaScript 代理,其行為就和普通對象一樣。不同的是,Vue 能夠攔截對響應式對象所有屬性的訪問和修改,以便進行依賴追蹤和觸發更新。
reactive();
括號內可定義「物件值」,也就是你要請 reactive
追蹤的參數。
我們先悄悄說一下 reactive
這個方式的綁定限制,之後會以範例帶大家來看看:
官方文件:它只能用於對象類型 (對象、數組和如 Map、Set 這樣的集合類型)。它不能持有如 string、number 或 boolean 這樣的原始類型。
不過,之前有提到 ref
也能夠綁定物件,那為什麼還需要 reactive
呢?
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>
以上做了什麼:
obj
物件,其中包含屬性 today
,並用 reactive
和 ref
兩種方式進行響應性綁定。reactiveObj
是通過 reactive
將 obj
轉換為一個 Proxy
物件,會使整個物件都具有響應性。refObj
是通過 ref
將 obj
包裝起來的,是一個 RefImpl 物件。瀏覽器上可以看到:
reactiveObj
:它是一個 Proxy
物件,內部有 today
屬性。我們複習一下昨天講到的 Proxy 概念:它是一個代理物件,內部會有攔截機制(get、set 等等方法)。
因此我們在模板使用 <h3>reactive example: {{ reactiveObj.today }}</h3>
時,會觸發內部 Proxy
的 get
機制,因此可取到 today
的值。
refObj
:是一個 RefImpl
物件,用 _value
值去追蹤內部的值,而 _value
內有一個 Proxy
物件。我們複習一下 ref
在模板中解包的 概念:
所以在這邊我們使用 <h3>ref example: {{ refObj.today }}</h3>
時:
refObj
這個 RefImpl
物件會自動解包(不用透過 _value
拿到 obj
)obj
是一個物件,所以我們透過 .today
再取得值。Proxy
物件並不等於原始物件Proxy
物件具響應性,原始物件不具備我們可以從中觀察:
<script setup>
import { reactive } from "vue";
const obj = { today: "今天是鐵人賽第 14 天,你還活著嗎" };
const reactiveObj = reactive(obj);
console.log(`Proxy 物件是否等於原始物件:`, obj === reactiveObj);
</script>
答案是 false
。
官方文件:只有代理對象是響應式的,更改原始對象不會觸發更新。因此,使用 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 分別去對 obj
、reactiveObj
的值做修改並將目前原始物件印出。
changeOriginal
不會觸發 UI 更新changeReactive
使用 reactiveObj
響應式物件修改的值會觸發 reactive(obj)
進而觸發 UI 更新。瀏覽器上可看見:
兩個按鈕點擊後 obj
物件中的值是有被更改的,唯獨差異是:直接用「原始物件」方式改的,「不會觸發」 UI 更新。
這邊也要提到為什麼用 reactiveObj.answer = "代理物件鼠掉了";
這個方法改值,{{ obj.answer }}
的 UI 也會更新?因為原始物件和代理物件是指向同一個記憶體的 因為 Proxy 物件是對原始物件的代理,當代理物件修改屬性時,這些更改會「直接反映到原始物件」上,所以修改代理物件的屬性會同步影響原始物件的屬性。
鼠掉了沒有關係,還是鼠出了電光一閃
(import 橘子の DOM Event 系列文 - 元素不可思議事件簿!很有趣 XD
官方文件:為保證訪問代理的一致性,對同一個原始對象調用 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
。
無論重複綁定多少次,還是會得到「同一個」代理物件:因此確保了響應式數據被正確控管。
當我們嘗試著綁定「數字」或「字串」時:
<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>
瀏覽器上會看到:
專案看起來並不會報錯,定義的值會正常渲染耶,但是出現了一些警告:
那我們修改值呢?
<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>
中的文本插值並沒有更新到最新的值!
官方文件:由於 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>
reactive()
追蹤的物件 state
。changeState
函式:{ count: 0 }
改為 { count: 1 }
。changeValue
函式:state.count++;
增加響應式物件屬性的值。可以看到:
count
的值會正常增加,介面會隨之更新。state
」的按鈕後,state
物件被全新的物件 { count: 1 }
替換,雖然可看到 count 的值已經被設為 1,介面卻不會更新,因為替換後導致 state
失去原本的響應性。<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>
來看看這段程式碼做了什麼:
const reactiveObj = reactive({ count: 0 });
:使用 reactive()
將 { count: 0 }
物件用響應式綁定為 reactiveObj
變數。let { count } = reactiveObj;
:解構代理物件中的屬性為 count
變數。count
變數++。reactiveObj
與解構後的變數 count
。<template>
中看兩者是否具備響應性。瀏覽器上會看到:
右邊:
reactiveObj
解構後,其內的 count
屬性 _value
還是 0,並非 1count
值為 ++ 過的 1。左邊我們使用:
<template>
<h3>已經斷開響應性連接的:{{ reactiveObj.count }}</h3>
<h3>被解構的變數:{{ count }}</h3>
</template>
觀察兩者是否具備響應性,得出 reactiveObj
被解構後其屬性值 count
,不具備響應性,所以還是 0。
而 count
則為執行 count+
+ 後的值 1。
官方文件:由於這些限制,我們建議使用 ref() 作為聲明響應式狀態的主要 API。
不過作者 尤雨溪 有在 這個影片 中談過:每個人使用的時候,多少帶點主觀成份,但只要你覺得它對是更適合你的想法,就不會有太大問題。主要還是看個人偏好和團隊共識。
覺得 reactive
它的細節和要注意的地方更多(腦袋爆炸中)
而兩者的區別和更進階的應用,應該之後實作的時候就會比較有感(應該吧 嗎)
https://github.com/Jamixcs/2024iThome-jamixcs/tree/main/src/components/day14