跳轉到內容

Vue 3 Composition API 最佳實踐 2026

Vue 3 Composition API

💚 組合勝於繼承。 Vue 3 的 Composition API 徹底改變了 Vue 組件的編寫方式。相比 Options API,它提供了更好的邏輯複用、更靈活的代碼組織和更優秀的 TypeScript 支持。本文帶你從入門到精通 Composition API,掌握 Vue 3 開發的最佳實踐。

本文將帶你係統掌握:

  • ✅ setup 與 <script setup> 語法
  • ✅ ref vs reactive 響應式系統
  • ✅ computed 與 watch 最佳實踐
  • ✅ Composables 組合式函數設計
  • ✅ 依賴注入(provide/inject)
  • ✅ 自定義指令與插件
  • ✅ 性能優化技巧
  • ✅ 項目架構最佳實踐

一、從 Options API 到 Composition API

1.1 為什麼需要 Composition API

問題Options APIComposition API
邏輯複用mixins(命名衝突)Composables(清晰)
代碼組織按選項分散按功能聚合
類型推斷優秀
Tree-shaking
代碼壓縮

1.2 對比示例

Options API:

vue
<script>
export default {
  data() {
    return { count: 0, name: '' }
  },
  computed: {
    double() { return this.count * 2 }
  },
  methods: {
    increment() { this.count++ }
  },
  mounted() {
    console.log('mounted')
  }
}
</script>

Composition API(<script setup>):

vue
<script setup lang="ts">
import { ref, computed, onMounted } from 'vue'

const count = ref(0)
const name = ref('')
const double = computed(() => count.value * 2)

function increment() {
  count.value++
}

onMounted(() => {
  console.log('mounted')
})
</script>

二、響應式系統

2.1 ref

ref 用於基本類型和需要整體替換的值:

typescript
import { ref } from 'vue'

const count = ref(0)
const name = ref('Vue')
const items = ref<string[]>([])

// 訪問和修改需要 .value
count.value++
name.value = 'Vue 3'
items.value.push('new item')

// 模板中自動解包,不需要 .value
// <div>{{ count }}</div>

2.2 reactive

reactive 用於對象類型,返回深層響應式代理:

typescript
import { reactive } from 'vue'

const state = reactive({
  user: { name: 'Alice', age: 25 },
  items: []
})

// 直接訪問,不需要 .value
state.user.name = 'Bob'
state.items.push({ id: 1 })

2.3 ref vs reactive 選擇

特性refreactive
適用類型任意對象/數組
訪問方式.value直接
解構保持響應式失去響應式
替換直接賦值需要 Object.assign
推薦場景基本類型、需替換表單、複雜對象

reactive 解構問題:

typescript
const state = reactive({ count: 0, name: 'Vue' })

// 解構後失去響應式!
const { count, name } = state  // 不是響應式的

// 使用 toRefs 保持響應式
import { toRefs } from 'vue'
const { count, name } = toRefs(state)  // 響應式

2.4 其他響應式 API

typescript
import { 
  shallowRef, shallowReactive,
  readonly, shallowReadonly,
  toRef, toRefs, toRaw,
  isRef, isReactive, isProxy
} from 'vue'

// shallowRef:只有 .value 是響應式的
const obj = shallowRef({ count: 0 })
obj.value.count = 1  // 不觸發更新
obj.value = { count: 1 }  // 觸發更新

// readonly:只讀代理
const original = reactive({ count: 0 })
const copy = readonly(original)

// toRaw:獲取原始對象
const raw = toRaw(reactiveObj)

// isRef:判斷是否為 ref
console.log(isRef(count))  // true

三、computed 與 watch

3.1 computed

typescript
import { ref, computed } from 'vue'

const firstName = ref('John')
const lastName = ref('Doe')

// 只讀計算屬性
const fullName = computed(() => `${firstName.value} ${lastName.value}`)

// 可寫計算屬性
const fullNameWritable = computed({
  get: () => `${firstName.value} ${lastName.value}`,
  set: (newVal: string) => {
    [firstName.value, lastName.value] = newVal.split(' ')
  }
})

3.2 computed 最佳實踐

typescript
// 避免在 computed 中產生副作用
// 錯誤示例
const bad = computed(() => {
  api.fetchData()  // 不要在 computed 中做異步操作
  return data.value
})

// 正確:用 watch 處理副作用
watch(source, () => {
  api.fetchData()
})

3.3 watch

typescript
import { ref, watch, watchEffect } from 'vue'

const count = ref(0)
const obj = reactive({ nested: { value: 0 } })

// 監聽 ref
watch(count, (newVal, oldVal) => {
  console.log(`${oldVal} -> ${newVal}`)
})

// 監聽 reactive 屬性(需要 getter 函數)
watch(
  () => obj.nested.value,
  (newVal, oldVal) => {
    console.log(`${oldVal} -> ${newVal}`)
  }
)

// 深度監聽
watch(
  obj,
  (newVal) => {
    console.log('對象變化', newVal)
  },
  { deep: true }
)

// 立即執行
watch(
  count,
  (newVal) => {
    console.log('立即執行', newVal)
  },
  { immediate: true }
)

3.4 watchEffect

typescript
import { watchEffect, ref } from 'vue'

const count = ref(0)
const name = ref('Vue')

// 自動追蹤依賴,立即執行
watchEffect(() => {
  console.log(`count: ${count.value}, name: ${name.value}`)
})

// 清理副作用
watchEffect((onCleanup) => {
  const timer = setTimeout(() => {
    console.log(count.value)
  }, 1000)
  
  onCleanup(() => {
    clearTimeout(timer)
  })
})

3.5 watch vs watchEffect

特性watchwatchEffect
依賴追蹤顯式指定自動追蹤
舊值可獲取不提供
立即執行immediate默認立即
適用於需要新舊值對比只需執行副作用

四、Composables 組合式函數

4.1 什麼是 Composables

Composables 是以 use 開頭的函數,封裝可複用的有狀態邏輯:

typescript
// composables/useCounter.ts
import { ref, computed } from 'vue'

export function useCounter(initialValue = 0) {
  const count = ref(initialValue)
  const double = computed(() => count.value * 2)
  
  function increment() { count.value++ }
  function decrement() { count.value-- }
  function reset() { count.value = initialValue }
  
  return { count, double, increment, decrement, reset }
}
vue
<script setup lang="ts">
import { useCounter } from '@/composables/useCounter'

const { count, double, increment } = useCounter(10)
</script>

4.2 實用 Composable:useMouse

typescript
// composables/useMouse.ts
import { ref, onMounted, onUnmounted } from 'vue'

export function useMouse() {
  const x = ref(0)
  const y = ref(0)
  
  function update(event: MouseEvent) {
    x.value = event.pageX
    y.value = event.pageY
  }
  
  onMounted(() => {
    window.addEventListener('mousemove', update)
  })
  
  onUnmounted(() => {
    window.removeEventListener('mousemove', update)
  })
  
  return { x, y }
}

4.3 實用 Composable:useFetch

typescript
// composables/useFetch.ts
import { ref, watchEffect, type Ref } from 'vue'

export function useFetch<T>(url: Ref<string> | string) {
  const data = ref<T | null>(null) as Ref<T | null>
  const error = ref<string | null>(null)
  const loading = ref(false)
  
  async function doFetch() {
    loading.value = true
    error.value = null
    data.value = null
    
    try {
      const response = await fetch(typeof url === 'string' ? url : url.value)
      if (!response.ok) throw new Error(response.statusText)
      data.value = await response.json()
    } catch (e) {
      error.value = (e as Error).message
    } finally {
      loading.value = false
    }
  }
  
  // 如果 url 是 ref,自動重新請求
  if (typeof url !== 'string') {
    watchEffect(doFetch)
  } else {
    doFetch()
  }
  
  return { data, error, loading, refresh: doFetch }
}

4.4 實用 Composable:useLocalStorage

typescript
// composables/useLocalStorage.ts
import { ref, watch, type Ref } from 'vue'

export function useLocalStorage<T>(
  key: string,
  defaultValue: T
): Ref<T> {
  const stored = localStorage.getItem(key)
  const data = ref<T>(
    stored ? JSON.parse(stored) : defaultValue
  ) as Ref<T>
  
  watch(
    data,
    (newVal) => {
      localStorage.setItem(key, JSON.stringify(newVal))
    },
    { deep: true }
  )
  
  return data
}
vue
<script setup>
import { useLocalStorage } from '@/composables/useLocalStorage'

const theme = useLocalStorage('theme', 'light')
const settings = useLocalStorage('settings', { sidebar: true, compact: false })
</script>

4.5 Composable 設計原則

  1. 命名規範:以 use 開頭
  2. 返回值:返回 ref 或 reactive,不要返回普通值
  3. 生命週期:在函數內註冊 onMounted
  4. 輸入參數:支持 ref 和普通值
  5. 無副作用:不在 composable 中直接操作 DOM
  6. 類型安全:使用泛型

五、依賴注入

5.1 provide / inject

typescript
// 父組件
import { provide, ref } from 'vue'
import type { InjectionKey, Ref } from 'vue'

// 定義 injection key(類型安全)
const countKey: InjectionKey<Ref<number>> = Symbol('count')

const count = ref(0)
provide(countKey, count)
typescript
// 子組件
import { inject } from 'vue'

const count = inject(countKey)  // 類型: Ref<number> | undefined
// 帶默認值
const count = inject(countKey, ref(0))

5.2 封裝 provider

typescript
// composables/useTheme.ts
import { ref, provide, inject, type InjectionKey, type Ref } from 'vue'

const themeKey: InjectionKey<Ref<string>> = Symbol('theme')

export function provideTheme(initial: string = 'light') {
  const theme = ref(initial)
  provide(themeKey, theme)
  return theme
}

export function useTheme() {
  const theme = inject(themeKey)
  if (!theme) {
    throw new Error('useTheme must be used within a ThemeProvider')
  }
  return theme
}
vue
<!-- App.vue -->
<script setup>
import { provideTheme } from '@/composables/useTheme'
const theme = provideTheme('dark')
</script>

<!-- Child.vue -->
<script setup>
import { useTheme } from '@/composables/useTheme'
const theme = useTheme()
</script>

六、生命週期鉤子

typescript
import {
  onBeforeMount, onMounted,
  onBeforeUpdate, onUpdated,
  onBeforeUnmount, onUnmounted,
  onErrorCaptured, onActivated, onDeactivated
} from 'vue'

// 組件掛載
onBeforeMount(() => console.log('before mount'))
onMounted(() => console.log('mounted'))

// 組件更新
onBeforeUpdate(() => console.log('before update'))
onUpdated(() => console.log('updated'))

// 組件卸載(清理資源)
onBeforeUnmount(() => console.log('before unmount'))
onUnmounted(() => {
  console.log('unmounted')
  // 清理定時器、事件監聽器等
  clearInterval(timer)
  window.removeEventListener('resize', handler)
})

// 錯誤捕獲
onErrorCaptured((err, instance, info) => {
  console.error('錯誤:', err)
  return false  // 阻止繼續傳播
})

七、<script setup> 最佳實踐

7.1 Props 和 Emits

vue
<script setup lang="ts">
// Props 定義
interface Props {
  title: string
  count?: number
  items?: string[]
}

const props = withDefaults(defineProps<Props>(), {
  count: 0,
  items: () => []
})

// Emits 定義
const emit = defineEmits<{
  (e: 'update', value: string): void
  (e: 'delete', id: number): void
}>()

function handleClick() {
  emit('update', 'new value')
}
</script>

7.2 defineModel(Vue 3.4+)

vue
<!-- 雙向綁定變得簡單 -->
<script setup lang="ts">
const model = defineModel<string>()
</script>

<template>
  <input v-model="model" />
</template>

<!-- 父組件使用 -->
<!-- <MyInput v-model="text" /> -->

7.3 defineExpose

vue
<script setup>
import { ref } from 'vue'

const count = ref(0)
function reset() { count.value = 0 }

// 暴露給父組件通過 ref 訪問
defineExpose({ count, reset })
</script>
vue
<!-- 父組件 -->
<script setup>
import { ref } from 'vue'
import Child from './Child.vue'

const childRef = ref()
// childRef.value?.count
// childRef.value?.reset()
</script>

7.4 useSlots 和 useAttrs

vue
<script setup>
import { useSlots, useAttrs } from 'vue'

const slots = useSlots()
const attrs = useAttrs()

console.log(slots.default)
console.log(attrs.class)
</script>

八、性能優化

8.1 v-once 和 v-memo

vue
<template>
  <!-- v-once: 只渲染一次 -->
  <div v-once>{{ expensiveComputed() }}</div>
  
  <!-- v-memo: 依賴不變時跳過更新 -->
  <div v-memo="[item.id, item.selected]">
    {{ item.name }}
  </div>
</template>

8.2 shallowRef / shallowReactive

typescript
// 大列表用 shallowRef,避免深層響應式開銷
const bigList = shallowRef<Item[]>([])

// 手動觸發更新
bigList.value = [...bigList.value, newItem]

8.3 異步組件與懶加載

typescript
import { defineAsyncComponent } from 'vue'

const AsyncComp = defineAsyncComponent(() => {
  return import('./HeavyComponent.vue')
})

// 帶選項
const AsyncCompWithOptions = defineAsyncComponent({
  loader: () => import('./HeavyComponent.vue'),
  loadingComponent: LoadingSpinner,
  errorComponent: ErrorDisplay,
  delay: 200,
  timeout: 3000
})

8.4 computed 緩存

typescript
// computed 自動緩存,避免重複計算
const expensive = computed(() => {
  return data.value.map(item => transform(item))
})

// 多處使用同一個 computed,只計算一次

8.5 虛擬列表

vue
<script setup>
import { useVirtualList } from '@vueuse/core'

const { list, containerProps, wrapperProps } = useVirtualList(
  bigArray,
  { itemHeight: 60 }
)
</script>

<template>
  <div v-bind="containerProps" style="height: 400px; overflow: auto">
    <div v-bind="wrapperProps">
      <div v-for="{ data, index } in list" :key="index" style="height: 60px">
        {{ data }}
      </div>
    </div>
  </div>
</template>

九、項目架構最佳實踐

9.1 目錄結構

src/
├── components/          # 通用組件
│   ├── common/         # 基礎組件
│   └── business/       # 業務組件
├── composables/         # 組合式函數
│   ├── useMouse.ts
│   ├── useFetch.ts
│   └── useAuth.ts
├── stores/             # Pinia 狀態管理
│   ├── user.ts
│   └── app.ts
├── utils/              # 工具函數
├── api/                # API 請求
├── types/              # 類型定義
├── views/              # 頁面
└── App.vue

9.2 Composable 與 Pinia 的關係

特性ComposablePinia Store
狀態範圍組件內全局
持久化可持久化
DevTools不集成集成
適用場景局部邏輯複用全局狀態管理
typescript
// stores/user.ts - 全局狀態
import { defineStore } from 'pinia'

export const useUserStore = defineStore('user', () => {
  const user = ref<User | null>(null)
  const token = ref<string>('')
  
  async function login(credentials: Credentials) {
    const res = await api.login(credentials)
    user.value = res.user
    token.value = res.token
  }
  
  return { user, token, login }
})
typescript
// composables/useFormValidation.ts - 局部邏輯複用
export function useFormValidation<T>(initial: T) {
  const errors = ref<Record<string, string>>({})
  const values = reactive({ ...initial })
  
  function validate(rules: ValidationRules) {
    errors.value = {}
    // 驗證邏輯
  }
  
  return { values, errors, validate }
}

十、VueUse 常用 Composables

VueUse 是最流行的 Composables 集合:

typescript
import {
  useLocalStorage,    // 本地存儲
  useMouse,           // 鼠標位置
  useEventListener,   // 事件監聽
  useIntersectionObserver,  // 交叉觀察
  useDebounceFn,      // 防抖
  useThrottleFn,      // 節流
  useClipboard,       // 剪貼板
  useDark,            // 暗色模式
  useMediaQuery,      // 媒體查詢
  useFullscreen,      // 全屏
  usePermission,      // 權限查詢
  useNetwork,         // 網絡狀態
  useTitle,           // 頁面標題
} from '@vueuse/core'

// 暗色模式
const isDark = useDark()
const toggleDark = useToggle(isDark)

// 防抖
const debouncedFn = useDebounceFn(() => {
  console.log('防抖執行')
}, 500)

// 剪貼板
const { text, copy, copied } = useClipboard()

十一、總結

Composition API 是 Vue 3 的核心特性:

  1. <script setup> 是編寫組件的標準方式
  2. ref/reactive 是響應式系統的基礎
  3. Composables 是邏輯複用的最佳方案
  4. provide/inject 實現跨層級通信
  5. 性能優化 善用 computed 緩存和 shallowRef
  6. VueUse 是不可或缺的工具庫

💚 掌握 Composition API,就掌握了 Vue 3 的精髓。


相關文章推薦:

最後更新於: