
善用 computed 
常聽說不要把複雜的表達式寫在 template 裡,應優先用 computed 裝起來再放到 template 中。
聽說歸聽說,苓膏龜苓膏,來試著探討看看吧。( ´ ▽ ` )ノ
TIP
Vue 官方 Style Guide 之「Priority B Rules: Strongly Recommended」中有「Simple expressions in templates」
不過規則還是要依照團隊內採用規範為主。ԅ( ˘ω˘ԅ)
聽說性能有差? 
設計一個繁重的計算方便比較差別,來看看 computed 與 template 結果。
utils.ts
export function getFibonacci(n: number): number {
  if (n <= 1)
    return n
  return getFibonacci(n - 1) + getFibonacci(n - 2)
}就是一個經典的費氏數列計算。
接著設計兩個元件,一個使用 computed,一個直接在 template 裡計算。
components/ComputedCalc.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
<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
<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 的時間差異。
範例程式碼在此
console 的內容如下
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 效果,只有在 reactive dependency 改變時才會重新計算。
所以不管 template 如何更新,都不會觸發額外的計算。
從這個結果來看,善用 computed 最大的優點是可以減少不必要的計算,提升效能。
性能之外呢? 
路人:「一般才不會有這麼繁重的計算,所以是不是沒差?(。-`ω´-)」
鱈魚:「的確,你說的沒錯。(´,,•ω•,,)」
雖然說元件中若有一堆不必要的計算,可能會有積少成多,導致性能不佳的部分。
但根據個人一直以來的開發經驗,主要的性能瓶頸都不是這類問題,通常都是 DOM 太多導致畫面卡頓。
「性能比較好」,這個理由個人覺得不夠充分,不過 computed 除了性能之外,還有其他優點,例如:
方便除錯 
配合 debugger,方便追蹤響應過程。
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 中,就可以在單元測試中測試。
<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」的理念有點類似。
除了更容易測試外,命名準確的 computed 也能提升程式碼的可讀性。
真的不好嗎?(*´・д・) 
所以把表達式放在 template 中,真的很不好嗎?
我自己是覺得下面這種情況,還是很可以接受的啦。ԅ(´∀` ԅ)
<template>
  <div :class="{ active: isActive }">
    <h1>{{ i + 1 }}</h1>
    <div :style="{ color: isActive ? 'red' : 'blue' }">
      安安你好
    </div>
  </div>
</template>這類例子要是用 computed 裝起來,命名也不太好想。
但下面這種就有點過分了。(。-`ω´-)
<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 裝起來吧。(*´∀`)~♥
<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 可以在背景執行的獨立執行緒 ,可以避免主執行緒被阻塞。
我的酷酷元件的落雪效果也是用 Web Worker 計算雪花動態與碰撞偵測。
即使雪花超過 10 萬個,FPS 很低,也不會讓網頁卡頓。( •̀ ω •́ )✧
所以我說那個支援度呢?
截至 2025/01/17 為止,支援度達到了 98.19%,基本上可以快樂使用!✧⁑。٩(ˊᗜˋ*)و✧⁕。
VueUse 有一個 useWebWorkerFn 可以簡化 Web Worker 的使用。
來試試看吧。੭ ˙ᗜ˙ )੭
components/WorkerCalc.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
<script setup>
import { ref } from 'vue'
import ComputedCalc from './components/ComputedCalc.vue'
import TemplateCalc from './components/TemplateCalc.vue'
import WorkerCalc from './components/WorkerCalc.vue'
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 的計算時間。
worker: 959.0419921875 ms
worker: 996.47998046875 ms
worker: 980.422119140625 ms可以注意到即使花了將近 1 秒計算,網頁依舊很流暢。✧⁑。٩(ˊᗜˋ*)و✧⁕。
總結 🐟 
- computed有著- Computed Caching的效果,只有在 reactive dependency 改變時才會重新計算,可以減少不必要的計算。
- computed除了性能之外,還有方便除錯、測試、提升可讀性等優點。
- 繁重的計算可以考慮交給 Web Worker,避免網頁卡到歪。