---
url: /blog-vue/vue-shallow-ref-and-ref.md
description: 熟悉 Vue 的朋友們一定知道 `ref` 是啥，一起來看看 `shallowRef` 和 `ref` 有甚麼差別吧！ԅ( ˘ω˘ԅ)
---

![vue-shallow-ref-and-ref](/vue-shallow-ref-and-ref.webp){.cover}

# 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.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 的使用場景

先來個工程師的好習慣，第一步先看看[文件寫甚麼](https://vuejs.org/api/reactivity-advanced.html)。

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

文件也給了具體例子：

* [Guide - Reduce Reactivity Overhead for Large Immutable Structures](https://vuejs.org/guide/best-practices/performance#reduce-reactivity-overhead-for-large-immutable-structures)
* [Guide - Integration with External State Systems](https://vuejs.org/guide/extras/reactivity-in-depth#integration-with-external-state-systems)

### 大型物件

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

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

考慮以下程式碼。

```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` 大約需要 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：

* [Bug: When Application is proxied by Vue 3(ref), an error occurs when rendering graphics](https://github.com/pixijs/pixijs/issues/10477)

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

::: tip
例如酷酷元件中的 [useBabylonScene](https://gitlab.com/side_project/chill-component/-/blob/main/src/composables/use-babylon-scene.ts?ref_type=heads#L67)，用於快速建立一個 3D 場景。其中的 babylon.js 相關物件就是使用 `shallowRef` 包裝。
:::

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

## 總結 🐟

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

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