善用 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 />
</template>
現在 console 會多出 worker 的計算時間。
worker: 959.0419921875 ms
worker: 996.47998046875 ms
worker: 980.422119140625 ms
可以注意到即使花了將近 1 秒計算,網頁依舊很流暢。✧⁑。٩(ˊᗜˋ*)و✧⁕。
總結 🐟
computed
有著Computed Caching
的效果,只有在 reactive dependency 改變時才會重新計算,可以減少不必要的計算。computed
除了性能之外,還有方便除錯、測試、提升可讀性等優點。- 繁重的計算可以考慮交給 Web Worker,避免網頁卡到歪。