開發的時候總會想怎麼才能改善效能?怎麼才能讓網頁跑得快?但是...真的是這樣嗎?
好多時候,「跑得更快」不如「設計得更好」,畢竟程式碼在未來還是有機會擴充維護,如果本身設計不良,就算框架、網速再快,也救不了浪費資源的程式,重點是萬一找戰犯發現是過去的自己,天呀!這該有多尷尬!不論是精神還是重構,嚴格說起來這也算是一個沉默成本呀!
今天就讓我們一起解析如何讓一個 composable 不只是能重用,更能 配合 Vue 3.6 的 runtime 最佳化,達到 低耦合 + 高性能 吧!
Vue 3.6 帶來更細粒度的依賴追蹤 (Alien Signals / Vapor Mode)。
它會在 runtime 自動分析哪些響應式值需要更新,因此 小而專注 的 composable 可以最大化框架優化:
設計方式 | 結果 |
---|---|
內部塞一堆 reactive 資料 | 每次改動都會觸發大範圍計算 |
只暴露需要的 ref | Vue 只追蹤真正被用到的值,效能最佳 |
重點:composable 的輸出越精準、依賴越明確,Vue 3.6 就越能幫你跑得快。
開始之前先來看個範例,似乎很忙碌!要做 API 請求、分頁、快取,導致任何小改動都可能觸發整個 effect 重新運算...
export function useMegaList() {
const list = ref<any[]>([])
const page = ref(1)
const filter = ref('')
const sortKey = ref('createdAt')
const loading = ref(false)
const cache = new Map<string, any>()
// API 請求 + 快取
const load = async () => {
loading.value = true
const key = `${page.value}-${filter.value}-${sortKey.value}`
if (cache.has(key)) {
list.value = cache.get(key)
} else {
const res = await fetch(`/api/list?page=${page.value}&filter=${filter.value}&sort=${sortKey.value}`)
const data = await res.json()
cache.set(key, data)
list.value = data
}
loading.value = false
}
// UI 相關
window.addEventListener('resize', () => console.log('resize'))
return { list, page, filter, sortKey, load }
}
那該怎拆呢?還是說一個 composable 就只能做一個功能,那不就要開很多 composable?
其實,重點不是一定要「只有一個功能」,而是每個 composable 應該有一個明確的核心目的,就像以下修改後的範例,是不是一眼望去更清楚知道在做什麼?
// 請求資料
export function useFetchList(query: () => string) {
const list = ref<any[]>([])
const loading = ref(false)
const load = async () => {
loading.value = true
const res = await fetch(`/api/list?${query()}`)
list.value = await res.json()
loading.value = false
}
return { list, loading, load }
}
// 快取
export function useCache() {
const cache = new Map<string, any>()
return {
get: (k: string) => cache.get(k),
set: (k: string, v: any) => cache.set(k, v)
}
}
// 分頁控制
export function usePagination() {
const page = ref(1)
const next = () => page.value++
const prev = () => page.value--
return { page, next, prev }
}
最近拆到後來發現:後端很常會寫單元測試確認方法邏輯,其實 composable 要拆多細主要在這段邏輯可以用單元測試獨立驗證嗎?
好的做法:每個 composable 專注一件事,然後在元件或更高階的 composable 內自由組合。
useFetch
)import { ref, onScopeDispose } from 'vue'
export function useFetch<T>(url: string) {
const data = ref<T | null>(null)
const loading = ref(false)
const error = ref<string | null>(null)
let controller: AbortController | null = null
const load = async () => {
loading.value = true
error.value = null
controller = new AbortController()
try {
const res = await fetch(url, { signal: controller.signal })
if (!res.ok) throw new Error(res.statusText)
data.value = await res.json()
} catch (err: any) {
if (err.name !== 'AbortError') error.value = err.message
} finally {
loading.value = false
}
}
// Vue 3.6 scope 清理:元件銷毀時自動中斷請求
onScopeDispose(() => controller?.abort())
return { data, loading, error, load }
}
usePolling
)import { onScopeDispose } from 'vue'
export function usePolling(fn: () => void, interval = 5000) {
const timer = setInterval(fn, interval)
onScopeDispose(() => clearInterval(timer))
}
useCache
)const cache = new Map<string, any>()
export function useCache<T>(key: string, fetcher: () => Promise<T>) {
const data = ref<T | null>(cache.get(key) ?? null)
const load = async () => {
if (!cache.has(key)) {
const result = await fetcher()
cache.set(key, result)
data.value = result
} else {
data.value = cache.get(key)
}
}
return { data, load }
}
在元件中:
<script setup lang="ts">
const { data, load } = useFetch<User[]>('/api/users')
const { load: cachedLoad } = useCache('users', load)
usePolling(cachedLoad, 3000) // 每 3 秒自動刷新,但有快取保護
</script>
ref
→ 最小依賴追蹤設計原則 | 好處 |
---|---|
單一職責 | 任何功能都能獨立升級或替換 |
輸出最小化 | Vue 3.6 只追蹤真正使用的響應值 |
作用域清理 | 避免記憶體洩漏、廢請求 |
Composable 組裝 | 功能可自由拼接,效能與維護兩全其美 |
Vue 3.6 的 runtime 優化只是加速器,真正決定效能與可維護性的,仍是我們如何設計 composable。
從今天開始,翻出過去的寫過的專案,嘗試把複雜邏輯拆成可擴展的小積木,這樣不僅能跟上 Vue 3.6 的快,更能讓專案在未來任何框架演進下都保持彈性。
明天深入討論 effectScope 與 getCurrentScope
,讓我們一起理解 Vue 內部如何管理這些作用域,進一步掌握框架級資源回收技巧。