Skip to content

vue-shallow-ref-and-ref

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

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

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

shallowRefref 有甚麼差別呢?

顧名思義,簡單來說 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-400/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"
  >
    <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>

以上程式的邏輯為:

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

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

例如以下 issue:

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

TIP

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

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

總結 🐟

目前為止的經驗,性能問題通常是 DOM 太多,幾乎不會是 ref 的問題,過早使用 shallowRef 反而容易搞出其他 Bug 喔。( ˙꒳​˙)

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