Vue 3 Composition API 最佳實踐 2026

💚 組合勝於繼承。 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 API | Composition API |
|---|---|---|
| 邏輯複用 | mixins(命名衝突) | Composables(清晰) |
| 代碼組織 | 按選項分散 | 按功能聚合 |
| 類型推斷 | 弱 | 優秀 |
| Tree-shaking | 差 | 好 |
| 代碼壓縮 | 差 | 好 |
1.2 對比示例
Options API:
<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>):
<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 用於基本類型和需要整體替換的值:
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 用於對象類型,返回深層響應式代理:
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 選擇
| 特性 | ref | reactive |
|---|---|---|
| 適用類型 | 任意 | 對象/數組 |
| 訪問方式 | .value | 直接 |
| 解構 | 保持響應式 | 失去響應式 |
| 替換 | 直接賦值 | 需要 Object.assign |
| 推薦場景 | 基本類型、需替換 | 表單、複雜對象 |
reactive 解構問題:
const state = reactive({ count: 0, name: 'Vue' })
// 解構後失去響應式!
const { count, name } = state // 不是響應式的
// 使用 toRefs 保持響應式
import { toRefs } from 'vue'
const { count, name } = toRefs(state) // 響應式2.4 其他響應式 API
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
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 最佳實踐
// 避免在 computed 中產生副作用
// 錯誤示例
const bad = computed(() => {
api.fetchData() // 不要在 computed 中做異步操作
return data.value
})
// 正確:用 watch 處理副作用
watch(source, () => {
api.fetchData()
})3.3 watch
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
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
| 特性 | watch | watchEffect |
|---|---|---|
| 依賴追蹤 | 顯式指定 | 自動追蹤 |
| 舊值 | 可獲取 | 不提供 |
| 立即執行 | 需 immediate | 默認立即 |
| 適用於 | 需要新舊值對比 | 只需執行副作用 |
四、Composables 組合式函數
4.1 什麼是 Composables
Composables 是以 use 開頭的函數,封裝可複用的有狀態邏輯:
// 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 }
}<script setup lang="ts">
import { useCounter } from '@/composables/useCounter'
const { count, double, increment } = useCounter(10)
</script>4.2 實用 Composable:useMouse
// 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
// 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
// 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
}<script setup>
import { useLocalStorage } from '@/composables/useLocalStorage'
const theme = useLocalStorage('theme', 'light')
const settings = useLocalStorage('settings', { sidebar: true, compact: false })
</script>4.5 Composable 設計原則
- 命名規範:以
use開頭 - 返回值:返回 ref 或 reactive,不要返回普通值
- 生命週期:在函數內註冊
onMounted等 - 輸入參數:支持 ref 和普通值
- 無副作用:不在 composable 中直接操作 DOM
- 類型安全:使用泛型
五、依賴注入
5.1 provide / inject
// 父組件
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)// 子組件
import { inject } from 'vue'
const count = inject(countKey) // 類型: Ref<number> | undefined
// 帶默認值
const count = inject(countKey, ref(0))5.2 封裝 provider
// 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
}<!-- 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>六、生命週期鉤子
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
<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+)
<!-- 雙向綁定變得簡單 -->
<script setup lang="ts">
const model = defineModel<string>()
</script>
<template>
<input v-model="model" />
</template>
<!-- 父組件使用 -->
<!-- <MyInput v-model="text" /> -->7.3 defineExpose
<script setup>
import { ref } from 'vue'
const count = ref(0)
function reset() { count.value = 0 }
// 暴露給父組件通過 ref 訪問
defineExpose({ count, reset })
</script><!-- 父組件 -->
<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
<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
<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
// 大列表用 shallowRef,避免深層響應式開銷
const bigList = shallowRef<Item[]>([])
// 手動觸發更新
bigList.value = [...bigList.value, newItem]8.3 異步組件與懶加載
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 緩存
// computed 自動緩存,避免重複計算
const expensive = computed(() => {
return data.value.map(item => transform(item))
})
// 多處使用同一個 computed,只計算一次8.5 虛擬列表
<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.vue9.2 Composable 與 Pinia 的關係
| 特性 | Composable | Pinia Store |
|---|---|---|
| 狀態範圍 | 組件內 | 全局 |
| 持久化 | 否 | 可持久化 |
| DevTools | 不集成 | 集成 |
| 適用場景 | 局部邏輯複用 | 全局狀態管理 |
// 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 }
})// 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 集合:
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 的核心特性:
<script setup>是編寫組件的標準方式- ref/reactive 是響應式系統的基礎
- Composables 是邏輯複用的最佳方案
- provide/inject 實現跨層級通信
- 性能優化 善用 computed 緩存和 shallowRef
- VueUse 是不可或缺的工具庫
💚 掌握 Composition API,就掌握了 Vue 3 的精髓。
相關文章推薦: