Skip to content

善用 computed

make-good-use-of-computed

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

聽說歸聽說,苓膏龜苓膏,來試著探討看看吧。( ´ ▽ ` )ノ

TIP

Vue 官方 Style Guide 之「Priority B Rules: Strongly Recommended」中有「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 的時間差異。

範例程式碼在此

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 效果,只有在 reactive dependency 改變時才會重新計算。

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

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

性能之外呢?

路人:「一般才不會有這麼繁重的計算,所以是不是沒差?(。-`ω´-)

鱈魚:「的確,你說的沒錯。(´,,•ω•,,)


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

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

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

方便除錯

配合 debugger,方便追蹤響應過程。

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」的理念有點類似。

除了更容易測試外,命名準確的 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 可以在背景執行的獨立執行緒 ,可以避免主執行緒被阻塞。

我的酷酷元件的落雪效果也是用 Web Worker 計算雪花動態與碰撞偵測。

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

所以我說那個支援度呢?

截至 2025/01/17 為止,支援度達到了 98.19%,基本上可以快樂使用!✧⁑。٩(ˊᗜˋ*)و✧⁕。

VueUse 有一個 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'

const toggle = ref(true)
</script>

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

  <worker-calc />
</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,避免網頁卡到歪。