---
url: /blog-vue/make-good-use-of-computed.md
description: 常聽說不要把複雜的表達式寫在 template 裡，應先用 computed 裝起來，來試試有甚麼差別吧。ლ（´∀`ლ）
---

![make-good-use-of-computed](/make-good-use-of-computed.webp){.cover}

# 善用 computed

常聽說不要把複雜的表達式寫在 template 裡，應優先用 computed 裝起來再放到 template 中。

聽說歸聽說，~~苓膏龜苓膏~~，來試著探討看看吧。%( ´ ▽ \` )ﾉ%

::: tip
Vue 官方 Style Guide 之「Priority B Rules: Strongly Recommended」中有「[Simple expressions in templates](https://vuejs.org/style-guide/rules-strongly-recommended.html#simple-expressions-in-templates)」

不過規則還是要依照團隊內採用規範為主。%ԅ( ˘ω˘ԅ)%
:::

## 聽說性能有差？

設計一個繁重的計算方便比較差別，來看看 computed 與 template 結果。

`utils.ts`

```ts
export function getFibonacci(n: number): number {
  if (n <= 1)
    return n
  return getFibonacci(n - 1) + getFibonacci(n - 2)
}
```

就是一個經典的費氏數列計算。

接著設計兩個元件，一個使用 computed，一個直接在 template 裡計算。

`components/ComputedCalc.vue`

```vue
<script setup lang="ts">
import { useIntervalFn } from '@vueuse/core'
import { computed, nextTick, ref } from 'vue'
import { getFibonacci } from '../utils'

const n = ref(30)
const key = ref('')

const result = computed(() => getFibonacci(n.value))

useIntervalFn(
  async () => {
    key.value = crypto.randomUUID()

    console.time('computed')
    await nextTick()
    console.timeEnd('computed')
  },
  3000,
  {
    immediateCallback: true,
  }
)
</script>

<template>
  <div>
    <h1>computed</h1>
    <div :key>
      {{ result }}
    </div>
  </div>
</template>
```

`components/TemplateCalc.vue`

```vue
<script setup lang="ts">
import { useIntervalFn } from '@vueuse/core'
import { nextTick, ref } from 'vue'
import { getFibonacci } from '../utils'

const n = ref(30)
const key = ref('')

useIntervalFn(
  async () => {
    key.value = crypto.randomUUID()

    console.time('template')
    await nextTick()
    console.timeEnd('template')
  },
  3000,
  {
    immediateCallback: true,
  }
)
</script>

<template>
  <div>
    <h1>template</h1>
    <div :key>
      {{ getFibonacci(n) }}
    </div>
  </div>
</template>
```

主要邏輯為：

* 透過 `useIntervalFn` 每 3 秒更新一次 `key`，觸發 template 更新
* `console.time` 計算 template 更新所需的時間
* `ComputedCalc.vue` 使用 `computed` 儲存 `getFibonacci` 結果
* `TemplateCalc.vue` 則直接在 `template` 裡呼叫 `getFibonacci`。

接著我們在 `App.vue` 中來測試這兩個元件。

`App.vue`

```vue
<script setup>
import { ref } from 'vue'
import ComputedCalc from './components/ComputedCalc.vue'
import TemplateCalc from './components/TemplateCalc.vue'

const toggle = ref(true)
</script>

<template>
  <input v-model="toggle" type="checkbox">切換</input>
  <computed-calc v-if="toggle" />
  <template-calc v-else />
</template>
```

切換 `checkbox` 會在 devtool 的 console 看到 computed 與 template 的時間差異。

範例程式碼[在此](https://stackblitz.com/edit/make-good-use-of-computed-rnmb827u?file=src%2FApp.vue)

console 的內容如下

```text
computed: 16.491943359375 ms
computed: 0.996826171875 ms

template: 12.89794921875 ms
template: 12.85302734375 ms

computed: 13.27294921875 ms
computed: 0.20703125 ms
computed: 0.18408203125 ms

template: 15.92578125 ms
template: 12.6689453125 ms
template: 12.741943359375 ms
```

可以注意到計算 `getFibonacci(30)` 的時間，兩者基本上差不多，最大的差別在於 `computed` 只有第一次計算時會花較多時間。

這就是官文文件提到的 [Computed Caching](https://vuejs.org/guide/essentials/computed#computed-caching-vs-methods) 效果，只有在 reactive dependency 改變時才會重新計算。

所以不管 template 如何更新，都不會觸發額外的計算。

從這個結果來看，善用 `computed` 最大的優點是可以減少不必要的計算，提升效能。

## 性能之外呢？

路人：「一般才不會有這麼繁重的計算，所以是不是沒差？%(。-\`ω´-)%」

鱈魚：「的確，你說的沒錯。%(´,,•ω•,,)%」

雖然說元件中若有一堆不必要的計算，可能會有積少成多，導致性能不佳的部分。

但根據個人一直以來的開發經驗，主要的性能瓶頸都不是這類問題，通常都是 DOM 太多導致畫面卡頓。

「性能比較好」，這個理由個人覺得不夠充分，不過 `computed` 除了性能之外，還有其他優點，例如：

### 方便除錯

配合 [debugger](https://vuejs.org/guide/extras/reactivity-in-depth.html#computed-debugging)，方便追蹤響應過程。

```ts
const plusOne = computed(() => count.value + 1, {
  onTrack(e) {
    // triggered when count.value is tracked as a dependency
    debugger
  },
  onTrigger(e) {
    // triggered when count.value is mutated
    debugger
  }
})
```

### 方便測試

`computed` 的內容放到 `defineExpose` 中，就可以在單元測試中測試。

```vue
<script setup>
import { computed, defineExpose, ref } from 'vue'
import { getFibonacci } from '../utils'

const n = ref(0)
const result = computed(() => getFibonacci(n.value))

defineExpose({ result })
</script>
```

這一點與 Style Guide 中的「[Simple computed properties](https://vuejs.org/style-guide/rules-strongly-recommended.html#simple-computed-properties)」的理念有點類似。

除了更容易測試外，命名準確的 `computed` 也能提升程式碼的可讀性。

## 真的不好嗎？(\*´･д･)

所以把表達式放在 `template` 中，真的很不好嗎？

我自己是覺得下面這種情況，還是很可以接受的啦。%ԅ(´∀\` ԅ)%

```vue
<template>
  <div :class="{ active: isActive }">
    <h1>{{ i + 1 }}</h1>

    <div :style="{ color: isActive ? 'red' : 'blue' }">
      安安你好
    </div>
  </div>
</template>
```

這類例子要是用 `computed` 裝起來，命名也不太好想。

但下面這種就有點過分了。%(。-\`ω´-)%

```vue
<template>
  <div>
    <div
      :style="{
        color: message.length > 10 ? 'red' : 'blue',
        fontSize: message.length > 10 ? '20px' : '16px',
        letterSpacing: message.length > 10 ? '2px' : '1px',
        fontWeight: message.length > 10 ? 'bold' : 'normal',
      }"
    >
      {{ message }}
    </div>

    總合為 {{
      data
        .filter((item) => item.name === 'cod')
        .map((item) => item.price)
        .reduce((acc, cur) => acc + cur, 0)
    }}
  </div>
</template>

<script setup lang="ts">
const message = ref('安安你好')
const data = ref([
  { name: 'cod', price: 18 },
  { name: 'cod', price: 20 },
  { name: 'cod', price: 22 },
])
</script>
```

建議用 `computed` 裝起來吧。%(\*´∀\`)~♥%

```vue
<template>
  <div>
    <div :style="messageStyle">
      {{ message }}
    </div>

    總合為 {{ total }}
  </div>
</template>

<script setup lang="ts">
import type { CSSProperties } from 'vue'
import { computed } from 'vue'

const message = ref('安安你好')
const messageStyle = computed<CSSProperties>(() => ({
  color: message.value.length > 10 ? 'red' : 'blue',
  fontSize: message.value.length > 10 ? '20px' : '16px',
  letterSpacing: message.value.length > 10 ? '2px' : '1px',
  fontWeight: message.value.length > 10 ? 'bold' : 'normal',
}))

const data = ref([
  { name: 'cod', price: 18 },
  { name: 'cod', price: 20 },
  { name: 'cod', price: 22 },
])
const total = computed(() => {
  return data.value
    .filter((item) => item.name === 'cod')
    .map((item) => item.price)
    .reduce((acc, cur) => acc + cur, 0)
})
</script>
```

## 同場加映

路人：「如果真的有繁重的計算需求，有甚麼方法嗎？」

鱈魚：「可以請打工仔支援。%ᕕ( ﾟ ∀。)ᕗ %」

路人：「...？%( ・ิω・ิ)%」

Web Worker 可以在背景執行的獨立執行緒 ，可以避免主執行緒被阻塞。

我的酷酷元件的[落雪效果](https://chillcomponent.codlin.me/components/bg-snow/)也是用 Web Worker 計算雪花動態與碰撞偵測。

即使雪花超過 10 萬個，FPS 很低，也不會讓網頁卡頓。%( •̀ ω •́ )✧%

::: tip 所以我說那個支援度呢？
截至 2025/01/17 為止，[支援度](https://caniuse.com/webworkers)達到了 98.19%，基本上可以快樂使用！✧⁑｡٩(ˊᗜˋ\*)و✧⁕｡
:::

VueUse 有一個 [`useWebWorkerFn`](https://vueuse.org/core/useWebWorkerFn/#usewebworkerfn) 可以簡化 Web Worker 的使用。

來試試看吧。%੭ ˙ᗜ˙ )੭%

`components/WorkerCalc.vue`

```vue
<script setup lang="ts">
import {
  useDateFormat,
  useIntervalFn,
  useTimestamp,
  useWebWorkerFn,
} from '@vueuse/core'
import { ref } from 'vue'
import { getFibonacci } from '../utils'

const time = useTimestamp()
const computedTime = useDateFormat(time, 'YYYY-MM-DD HH:mm:ss SSS')
const result = ref(0)

const { workerFn } = useWebWorkerFn((n: number) => getFibonacci(n), {
  timeout: 50000,
  localDependencies: [getFibonacci],
})

useIntervalFn(
  async () => {
    console.time('worker')
    result.value = await workerFn(40)
    console.timeEnd('worker')
  },
  3000,
  {
    immediateCallback: true,
  }
)
</script>

<template>
  <div>
    <h1>worker</h1>
    <div>
      {{ result }}
    </div>

    <div>{{ computedTime }}</div>
  </div>
</template>
```

每 3 秒計算一次 n=40 的費氏數列，並且顯示目前時間。

接著在 `App.vue` 中加入 `WorkerCalc`。

`App.vue`

```vue
<script setup>
import { ref } from 'vue'
import ComputedCalc from './components/ComputedCalc.vue'
import TemplateCalc from './components/TemplateCalc.vue'
import WorkerCalc from './components/WorkerCalc.vue' // [!code ++]

const toggle = ref(true)
</script>

<template>
  <input v-model="toggle" type="checkbox">切換</input>
  <computed-calc v-if="toggle" />
  <template-calc v-else />

  <worker-calc /> // [!code ++]
</template>
```

現在 console 會多出 worker 的計算時間。

```text
worker: 959.0419921875 ms
worker: 996.47998046875 ms
worker: 980.422119140625 ms
```

可以注意到即使花了將近 1 秒計算，網頁依舊很流暢。%✧⁑｡٩(ˊᗜˋ\*)و✧⁕｡%

## 總結 🐟

* `computed` 有著 `Computed Caching` 的效果，只有在 reactive dependency 改變時才會重新計算，可以減少不必要的計算。
* `computed` 除了性能之外，還有方便除錯、測試、提升可讀性等優點。
* 繁重的計算可以考慮交給 Web Worker，避免網頁卡到歪。
