iT邦幫忙

2025 iThome 鐵人賽

DAY 17
0
Vue.js

Vue3.6 的革新:深入理解 Composition API系列 第 17

Day 17: Composable 的「可擴展設計」與 Vue 3.6 新能力

  • 分享至 

  • xImage
  •  

開發的時候總會想怎麼才能改善效能?怎麼才能讓網頁跑得快?但是...真的是這樣嗎?

好多時候,「跑得更快」不如「設計得更好」,畢竟程式碼在未來還是有機會擴充維護,如果本身設計不良,就算框架、網速再快,也救不了浪費資源的程式,重點是萬一找戰犯發現是過去的自己,天呀!這該有多尷尬!不論是精神還是重構,嚴格說起來這也算是一個沉默成本呀!

今天就讓我們一起解析如何讓一個 composable 不只是能重用,更能 配合 Vue 3.6 的 runtime 最佳化,達到 低耦合 + 高性能 吧!

Vue 3.6 與 composable 的新默契


Vue 3.6 帶來更細粒度的依賴追蹤 (Alien Signals / Vapor Mode)。

它會在 runtime 自動分析哪些響應式值需要更新,因此 小而專注 的 composable 可以最大化框架優化:

設計方式 結果
內部塞一堆 reactive 資料 每次改動都會觸發大範圍計算
只暴露需要的 ref Vue 只追蹤真正被用到的值,效能最佳

重點:composable 的輸出越精準、依賴越明確,Vue 3.6 就越能幫你跑得快。

單一職責:composable 不該「包山包海」


開始之前先來看個範例,似乎很忙碌!要做 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 專注一件事,然後在元件或更高階的 composable 內自由組合。

Step 1:基礎請求 (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 }
}

Step 2:定時輪詢 (usePolling)

import { onScopeDispose } from 'vue'

export function usePolling(fn: () => void, interval = 5000) {
  const timer = setInterval(fn, interval)
  onScopeDispose(() => clearInterval(timer))
}

Step 3:快取結果 (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>
  • 彈性組合:輪詢、快取、請求完全解耦
  • Vue 3.6 最佳化:每個 composable 只輸出必要的 ref → 最小依賴追蹤
  • Scope 自動清理:離開頁面後自動停止輪詢、取消請求
設計原則 好處
單一職責 任何功能都能獨立升級或替換
輸出最小化 Vue 3.6 只追蹤真正使用的響應值
作用域清理 避免記憶體洩漏、廢請求
Composable 組裝 功能可自由拼接,效能與維護兩全其美

小結


Vue 3.6 的 runtime 優化只是加速器,真正決定效能與可維護性的,仍是我們如何設計 composable。

從今天開始,翻出過去的寫過的專案,嘗試把複雜邏輯拆成可擴展的小積木,這樣不僅能跟上 Vue 3.6 的快,更能讓專案在未來任何框架演進下都保持彈性。

明天深入討論 effectScope 與 getCurrentScope,讓我們一起理解 Vue 內部如何管理這些作用域,進一步掌握框架級資源回收技巧。


上一篇
Day 16: 讓組合更快的關鍵 - 寫出真正可拼裝的 composable
系列文
Vue3.6 的革新:深入理解 Composition API17
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言