Vue 的 shallowRef 是啥?和 ref 有甚麼差別?
熟悉 Vue 的朋友們一定知道 ref
是啥。ԅ( ˘ω˘ԅ)
不知道大家有沒有注意到 Vue 還有一個名字有點像的 API,叫做 shallowRef
。
shallowRef
和 ref
有甚麼差別呢?
顧名思義,簡單來說 shallowRef
只對淺層(第一層)進行響應式處理。
也就是說以下程式碼。
<script setup>
import { shallowRef } from 'vue'
const shallowData = shallowRef({ cod: [] })
async function updateShallowData() {
shallowData.value.cod.push('fish')
}
</script>
<template>
<div>
shallowData: {{ shallowData.cod.length }}
</div>
<button @click="updateShallowData">
更新資料
</button>
</template>
當我們點擊「更新資料」按鈕時,{{ shallowData.cod.length }}
部分不會被更新,文字會保持 shallowData: 0。
除非 updateShallowData
內容改成:
async function updateShallowData() {
const cod = [...shallowData.value.cod, 'fish'];
shallowData.value = { cod }
}
這樣就會因為第一層,也就是 shallowData.value
的值被改變,才會觸發響應,讓畫面更新。
或者使用 triggerRef
觸發響應也可以。
async function updateShallowData() {
shallowData.value.cod.push('fish')
triggerRef(shallowData)
}
看到這裡你可能會想「怎麼搞得更麻煩了?ლ(´口`ლ)」
別急別急,讓我們來看看為甚麼需要 shallowRef
這個玩意兒。
shallowRef 的使用場景
先來個工程師的好習慣,第一步先看看文件寫甚麼。
其實文件說得很清楚,主要目的是為了「性能考量」與「外部狀態管理整合」。
文件也給了具體例子:
- Guide - Reduce Reactivity Overhead for Large Immutable Structures
- Guide - Integration with External State Systems
大型物件
對整個龐大物件進行響應式處理,會導致性能下降。
只看文字說明不夠具體,讓我們用程式碼證明看看吧。
考慮以下程式碼。
<script setup>
import {
ref, shallowRef,
watch, triggerRef,
nextTick
} from 'vue'
/** 建立一個 10 個 key,每個 value 矩陣長度 1000 的大型物件 */
function createData() {
const data = {
cod: [],
}
for (let i = 0; i < 10; i++) {
const key = crypto.randomUUID();
data[key] = new Array(1000).fill('fish');
}
return data;
}
/** 使用 ref 包裝的資料 */
const data = ref(createData())
watch(data, () => {
console.log('[watch] data');
}, { deep: true })
async function updateData() {
console.time('updateData');
data.value.cod.push('fish')
await nextTick();
console.timeEnd('updateData');
}
/** 使用 shallowRef 包裝的資料 */
const shallowData = shallowRef(createData())
watch(shallowData, () => {
console.log('[watch] shallowData');
}, { deep: true })
async function updateShallowData() {
console.time('updateShallowData');
shallowData.value.cod.push('fish')
triggerRef(shallowData)
await nextTick();
console.timeEnd('updateShallowData');
}
</script>
<template>
<div>
data: {{ data.cod.length }}
</div>
<button @click="updateData">
更新資料
</button>
<hr>
<div>
shallowData: {{ shallowData.cod.length }}
</div>
<button @click="updateShallowData">
更新資料
</button>
</template>
以上程式的邏輯為:
createData
建立一個 10 個 key,每個 value 為長度 1000 的大型物件。- 分別使用
ref
和shallowRef
對createData
的結果進行包裝。 nextTick
表示觸發響應資料變更、等待 DOM 下次更新後,接著配合console.time
計算更新資料所需時間。
現在大家可以點擊看看兩者的「更新資料」按鈕,看看更新資料所需時間。
會在 console 中看到以下資訊。
[watch] data
updateData: 7.468994140625 ms
[watch] shallowData
updateShallowData: 0.60498046875 ms
可以注意到 shallowData
的更新時間比 data
快了近 10 倍!( •̀ ω •́ )✧
路人:「雖然差 10 倍,但實際上也才花了 7ms,平常也用不到這麼大的物件,這樣是不是不用 shallowRef
也無所謂啊?(*´・д・)?」
鱈魚:「你說沒錯 (´,,•ω•,,),平常很難遇到大型物件,不過還要是注意一些情境。」
配合第三方套件
當你使用 pixi、babylon.js 這類套件時,套件本身會建立各種複雜物件,例如:Engine、Scene、Mesh 等等。
雖然一般情況下,完全不需要 ref
,直接使用即可。
// ❌ 不需要 ref 包裝
const engine = ref(new Engine(canvas, true));
// ✅
const engine = new Engine(canvas, true);
但是有時候還是需要響應怎麼辦?這個時候就可以用 shallowRef
了!੭ ˙ᗜ˙ )੭
直接使用 ref
包裝,會讓畫面會卡到爆炸喔。
除了卡到炸以外,ref
的 Proxy
還有可能破壞套件的內部邏輯,導致套件無法正常運作。
例如以下 issue:
可以看到裡面提到的解法也是用 shallowRef
解決。◝( •ω• )◟
shallowRef
包裝。再進階一點還可以考慮使用 customRef
,不過這個就留到未來有機會再聊啦。( ´ ▽ ` )ノ
總結 🐟
shallowRef
只對第一層進行響應式處理,適合大型物件。- 配合第三方套件的複雜物件時,使用
shallowRef
可以避免破壞物件內部邏輯或拖垮性能。