Vue 的 shallowRef 是啥?和 ref 有甚麼差別?

vue-shallow-ref-and-ref

熟悉 Vue 的朋友們一定知道 ref 是啥。ԅ( ˘ω˘ԅ)

不知道大家有沒有注意到 Vue 還有一個名字有點像的 API,叫做 shallowRef

shallowRefref 有甚麼差別呢?

顧名思義,簡單來說 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 的使用場景

先來個工程師的好習慣,第一步先看看文件寫甚麼

其實文件說得很清楚,主要目的是為了「性能考量」與「外部狀態管理整合」。

文件也給了具體例子:

大型物件

對整個龐大物件進行響應式處理,會導致性能下降。

只看文字說明不夠具體,讓我們用程式碼證明看看吧。

考慮以下程式碼。

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

範例程式碼

以上程式的邏輯為:

  1. createData 建立一個 10 個 key,每個 value 為長度 1000 的大型物件。
  2. 分別使用 refshallowRefcreateData 的結果進行包裝。
  3. 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 包裝,會讓畫面會卡到爆炸喔。

除了卡到炸以外,refProxy 還有可能破壞套件的內部邏輯,導致套件無法正常運作。

例如以下 issue:

可以看到裡面提到的解法也是用 shallowRef 解決。◝( •ω• )◟

例如酷酷元件中的 useBabylonScene,用於快速建立一個 3D 場景。其中的 babylon.js 相關物件就是使用 shallowRef 包裝。

再進階一點還可以考慮使用 customRef,不過這個就留到未來有機會再聊啦。( ´ ▽ ` )ノ

總結 🐟

  • shallowRef 只對第一層進行響應式處理,適合大型物件。
  • 配合第三方套件的複雜物件時,使用 shallowRef 可以避免破壞物件內部邏輯或拖垮性能。