
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>
<div>
shallowData: {{ shallowData.cod.length }}
</div>
<button
class="bg-gray-400/50! duration-100 p-1! px-3! rounded! active:bg-gray-400/80!"
@click="updateShallowData"
>
更新資料
</button>
</div>
</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
大型物件
對整個龐大物件進行響應式處理,會導致性能下降。
只看文字說明不夠具體,讓我們用程式碼證明看看吧。
考慮以下程式碼。
<template>
<div
ref="exRef"
class="grid md:grid-cols-2 divide-y md:divide-y-0 md:divide-x divide-gray-100"
>
<div class="p-8 flex flex-col items-center space-y-6">
<span class="px-5 py-2 text-xs font-semibold bg-blue-100 text-blue-700 rounded-md">
Ref
</span>
<div class="text-xs uppercase tracking-wider mb-1">
耗時
</div>
<div class="text-3xl font-mono font-bold ">
{{ dataCost }} <span class="text-sm font-normal">ms</span>
</div>
</div>
<div class="p-8 flex flex-col items-center space-y-6">
<span class="px-5 py-2 text-xs font-semibold bg-emerald-100 text-emerald-700 rounded-md">
Shallow Ref
</span>
<div class="text-xs uppercase tracking-wider mb-1">
耗時
</div>
<div class="text-3xl font-mono font-bold ">
{{ shallowDataCost }} <span class="text-sm font-normal">ms</span>
</div>
</div>
</div>
</template>
<script setup>
import { useIntersectionObserver, useRafFn } from '@vueuse/core'
import { ref, shallowRef, triggerRef, useTemplateRef } from 'vue'
// 建立 1 萬筆資料
function createData() {
return {
list: Array.from({ length: 10000 }).map((_, i) => ({ id: i, val: 0 })),
}
}
const data = ref(createData())
const dataCost = ref('0.00')
function tortureDeep() {
const start = performance.now()
// 每一次 .val 讀取和寫入都會經過 Proxy 的攔截
data.value.list.forEach((item) => {
item.val++
})
const end = performance.now()
dataCost.value = (end - start).toFixed(2)
}
const shallowData = shallowRef(createData())
const shallowDataCost = ref('0.00')
function tortureShallow() {
const start = performance.now()
// 操作 raw object,完全沒有 Proxy 介入,速度等同於原生 JS
shallowData.value.list.forEach((item) => {
item.val++
})
triggerRef(shallowData)
const end = performance.now()
shallowDataCost.value = (end - start).toFixed(2)
}
const exRef = useTemplateRef('exRef')
const ticker = useRafFn(() => {
tortureShallow()
tortureDeep()
}, {
immediate: false,
fpsLimit: 5,
})
useIntersectionObserver(exRef, ([entry]) => {
entry.isIntersecting ? ticker.resume() : ticker.pause()
}, {
immediate: true,
})
</script>以上程式的邏輯為:
createData建立一個長度 10000 的大型物件。- 分別使用
ref和shallowRef對createData的結果進行包裝。 performance.now()紀錄更新資料所需時間。
在我的電腦上 ref 大約需要 7ms,shallowRef 大約需要 0.1ms。
可以注意到 shallowData 的更新時間比 data 快了近 70 倍!( •̀ ω •́ )✧
路人:「雖然差 70 倍,但實際上也才花了 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 解決。◝( •ω• )◟
TIP
例如酷酷元件中的 useBabylonScene,用於快速建立一個 3D 場景。其中的 babylon.js 相關物件就是使用 shallowRef 包裝。
再進階一點還可以考慮使用 customRef,不過這個就留到未來有機會再聊啦。( ´ ▽ ` )ノ
總結 🐟
目前為止的經驗,性能問題通常是 DOM 太多,幾乎不會是 ref 的問題,過早使用 shallowRef 反而容易搞出其他 Bug 喔。( ˙꒳˙)
shallowRef只對第一層進行響應式處理,適合複雜物件。- 配合第三方套件的複雜物件時,使用
shallowRef可以避免破壞物件內部邏輯或拖垮性能。