Skip to content

vue-shallow-ref-and-ref

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

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

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

shallowRef 和 ref 有甚麼差別呢?

顧名思義,簡單來說 shallowRef 只對淺層(第一層)進行響應式處理。

也就是說以下程式碼。

vue
<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-300/50 dark:bg-gray-600/50 duration-100 p-1! px-3! rounded! active:bg-gray-400/80"
      @click="updateShallowData"
    >
      更新資料
    </button>
  </div>
</template>
shallowData: 0

當我們點擊「更新資料」按鈕時,{{ shallowData.cod.length }} 部分不會被更新,文字會保持 shallowData: 0。

除非 updateShallowData 內容改成:

ts
async function updateShallowData() {
  const cod = [...shallowData.value.cod, 'fish']
  shallowData.value = { cod }
}

這樣就會因為第一層,也就是 shallowData.value 的值被改變,才會觸發響應,讓畫面更新。

或者使用 triggerRef 觸發響應也可以。

ts
async function updateShallowData() {
  shallowData.value.cod.push('fish')
  triggerRef(shallowData)
}

看到這裡你可能會想「怎麼搞得更麻煩了?ლ(´口`ლ)」

別急別急,讓我們來看看為甚麼需要 shallowRef 這個玩意兒。

shallowRef 的使用場景 ​

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

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

文件也給了具體例子:

大型物件 ​

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

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

考慮以下程式碼。

vue
<template>
  <div
    ref="exRef"
    class="grid md:grid-cols-2 divide-y md:divide-y-0 md:divide-x divide-gray-100 dark:divide-gray-800"
  >
    <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 dark:bg-blue-900/40 text-blue-700 dark:text-blue-300 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 dark:bg-emerald-900/40 text-emerald-700 dark:text-emerald-300 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>

以上程式的邏輯為:

  1. createData 建立一個長度 10000 的大型物件。
  2. 分別使用 ref 和 shallowRef 對 createData 的結果進行包裝。
  3. performance.now() 紀錄更新資料所需時間。
Ref
耗時
0.00 ms
Shallow Ref
耗時
0.00 ms

在我的電腦上 ref 大約需要 7ms,shallowRef 大約需要 0.1ms。

可以注意到 shallowData 的更新時間比 data 快了近 70 倍!( •̀ ω •́ )✧


路人:「雖然差 70 倍,但實際上也才花了 7ms,平常也用不到這麼大的物件,這樣是不是不用 shallowRef 也無所謂啊?(*´・д・)?」

鱈魚:「你說沒錯 (*´,,•ω•,,),平常很難遇到大型物件,不過還要是注意一些情境。」

配合第三方套件 ​

當你使用 pixi、babylon.js 這類套件時,套件本身會建立各種複雜物件,例如:Engine、Scene、Mesh 等等。

雖然一般情況下,完全不需要 ref,直接使用即可。

ts
// ❌ 不需要 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 可以避免破壞物件內部邏輯或拖垮性能。