Vue 3 Composition API深度指南

Vue 3 Composition API深度指南

概述與背景

Vue 3引入的Composition API是Vue框架的一次重大革新。相比Options API,它提供了更靈活的代碼組織方式、更好的TypeScript支持,以及更強大的邏輯復用能力。本指南將帶你深入理解Composition API的核心概念,並掌握其在實際項目中的應用。

graph TB
    subgraph Vue 3 Composition API架構
        A[setup函數] --> B[響應式系統]
        A --> C[生命週期鉤子]
        A --> D[Composables]
        
        B --> B1[ref]
        B --> B2[reactive]
        B --> B3[computed]
        
        C --> C1[onMounted]
        C --> C2[onUnmounted]
        C --> C3[watch/watchEffect]
        
        D --> D1[useUser]
        D --> D2[useCart]
        D --> D3[useSearch]
    end

    style A fill:#42b883
    style B fill:#c8e6c9
    style C fill:#c8e6c9
    style D fill:#c8e6c9
graph LR
    subgraph Options API vs Composition API
        A[Options API] --> A1[邏輯分散]
        A --> A2[mixin復用困難]
        A --> A3[TypeScript支持弱]
        
        B[Composition API] --> B1[邏輯集中]
        B --> B2[Composables復用]
        B --> B3[類型推導完善]
    end

    style A fill:#ffcdd2
    style B fill:#c8e6c9

為什麼選擇Composition API?

Options API的局限性

<!-- 大型組件的痛點 -->
<script>
export default {
  data() {
    return {
      // 用戶相關
      user: null,
      userLoading: false,
      // 購物車相關
      cart: [],
      cartLoading: false,
      // 搜索相關
      searchQuery: '',
      searchResults: []
    }
  },
  computed: {
    // 用戶相關
    userName() { /* ... */ },
    // 購物車相關
    cartTotal() { /* ... */ },
    // 搜索相關
    filteredResults() { /* ... */ }
  },
  methods: {
    // 用戶相關
    fetchUser() { /* ... */ },
    updateUser() { /* ... */ },
    // 購物車相關
    addToCart() { /* ... */ },
    removeFromCart() { /* ... */ },
    // 搜索相關
    search() { /* ... */ }
  },
  watch: {
    // 用戶相關
    user() { /* ... */ },
    // 購物車相關
    cart() { /* ... */ },
    // 搜索相關
    searchQuery() { /* ... */ }
  }
}
</script>

問題

  1. 邏輯分散:相關代碼分散在不同選項中
  2. 難以維護:大型組件需要頻繁滾動
  3. 復用困難:mixin存在命名衝突和來源不明問題

Composition API的優勢

<script setup>
// 用戶邏輯 - 所有相關代碼集中在一起
const { user, loading, fetchUser } = useUser()

// 購物車邏輯 - 獨立且可復用
const { cart, total, addToCart } = useCart()

// 搜索邏輯 - 清晰的邏輯邊界
const { query, results, search } = useSearch()
</script>

兩種API對比

維度Options APIComposition API
代碼組織按選項分散按功能聚合
TypeScript支持較弱原生支持
邏輯復用Mixin(有缺陷)Composables(優雅)
Tree-shaking不支持完全支持
代碼壓縮一般更好
學習曲線平緩較陡
適用場景小型組件大型/複雜組件

核心概念

響應式系統架構

┌─────────────────────────────────────┐
│         Composition API             │
├─────────────────────────────────────┤
│  ref() / reactive()                 │ ← 創建響應式數據
│  computed()                         │ ← 計算屬性
│  watch() / watchEffect()            │ ← 副作用監聽
│  toRef() / toRefs()                 │ ← 響應式轉換
└─────────────────────────────────────┘

┌─────────────────────────────────────┐
│        Reactivity System            │
│  (Proxy-based reactivity)           │
└─────────────────────────────────────┘

┌─────────────────────────────────────┐
│       Component Rendering           │
│  (Virtual DOM + Diff Algorithm)     │
└─────────────────────────────────────┘

基本術語

響應式對象(Reactive)

  • 使用 reactive() 創建
  • 深度響應式(嵌套對象也響應)
  • 僅支持對象類型
  • 返回Proxy代理對象

引用對象(Ref)

  • 使用 ref() 創建
  • 可以包裝任何類型
  • 通過 .value 訪問
  • 在模板中自動解包

計算屬性(Computed)

  • 基於其他響應式數據派生
  • 自動緩存,依賴不變不重新計算
  • 支持可寫計算屬性

副作用(Effect)

  • 響應式數據變化時執行
  • 包括 watchEffectwatch
  • 用於數據同步、API調用等

實戰步驟

第一步:setup函數與響應式基礎

1.1 <script setup> 語法糖

<template>
  <div>
    <p>計數: {{ count }}</p>
    <p>雙倍: {{ doubleCount }}</p>
    <button @click="increment">+1</button>
    <button @click="decrement">-1</button>
  </div>
</template>

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

// 響應式狀態
const count = ref(0)

// 計算屬性
const doubleCount = computed(() => count.value * 2)

// 方法
function increment() {
  count.value++
}

function decrement() {
  count.value--
}

// 所有頂層綁定自動暴露給模板
// 無需 return { count, doubleCount, increment, decrement }
</script>

等價的傳統寫法

<script>
import { ref, computed } from 'vue'

export default {
  setup() {
    const count = ref(0)
    const doubleCount = computed(() => count.value * 2)
    
    function increment() {
      count.value++
    }
    
    function decrement() {
      count.value--
    }
    
    // 必須顯式返回
    return {
      count,
      doubleCount,
      increment,
      decrement
    }
  }
}
</script>

1.2 ref vs reactive

ref的特點

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

// 基本類型
const count = ref(0)
const message = ref('Hello')

// 對象(內部創建響應式代理)
const user = ref({ name: 'Alice', age: 25 })

// 訪問和修改
console.log(count.value)  // 0
count.value = 10

console.log(user.value.name)  // 'Alice'
user.value.name = 'Bob'

// 重新賦值整個對象(reactive做不到)
user.value = { name: 'Charlie', age: 30 }

// 檢查是否為ref
console.log(isRef(count))  // true
</script>

<template>
  <!-- ref在模板中自動解包,無需.value -->
  <div>{{ count }}</div>
  <div>{{ user.name }}</div>
</template>

reactive的特點

<script setup>
import { reactive, isReactive } from 'vue'

// 僅支持對象類型
const state = reactive({
  items: [],
  loading: false,
  filters: {
    category: 'all',
    price: { min: 0, max: 1000 }
  }
})

// 深度響應式
state.items.push({ id: 1, name: 'Product' })
state.filters.price.max = 500  // 也會觸發更新

// 訪問無需.value
console.log(state.items)
state.loading = true

// 檢查是否為reactive
console.log(isReactive(state))  // true

// ⚠️ 注意:解構會失去響應性
const { items, loading } = state  // ❌ 失去響應性

// ✅ 使用toRefs保持響應性
import { toRefs } from 'vue'
const { items, loading } = toRefs(state)
</script>

選擇建議

場景推薦使用原因
基本類型(number、string、boolean)refreactive不支持
需要重新賦值整個對象ref可以.value重新賦值
組件內部狀態reactive更自然,無需.value
組合函數返回值ref調用者可以解包
表單數據reactive結構清晰
列表數據ref([])方便重新賦值

第二步:深入響應式系統

2.1 響應式工具函數

<script setup>
import { 
  ref, 
  reactive, 
  computed,
  watch,
  watchEffect,
  toRef,
  toRefs,
  unref,
  isRef,
  shallowRef,
  shallowReactive
} from 'vue'

// ==================== toRefs ====================
const state = reactive({
  firstName: 'John',
  lastName: 'Doe',
  email: 'john@example.com'
})

// 解構保持響應性
const { firstName, lastName, email } = toRefs(state)

// 每個屬性都是ref
console.log(firstName.value)  // 'John'
firstName.value = 'Jane'  // 會更新state.firstName

// ==================== toRef ====================
// 創建單個屬性的ref
const emailRef = toRef(state, 'email')

// ==================== unref ====================
// 解包ref,如果不是ref則原樣返回
const count = ref(10)
const plainValue = unref(count)  // 10
const sameValue = unref(20)      // 20

// ==================== shallow版本 ====================
// 淺層響應式(僅頂層響應)
const shallow = shallowReactive({
  nested: { value: 1 }
})
shallow.nested.value = 2  // 不觸發更新

const shallowCount = shallowRef({ value: 1 })
shallowCount.value.nested = 2  // 不觸發更新
shallowCount.value = { value: 2 }  // 觸發更新

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

// 可寫計算屬性
const fullNameWritable = computed({
  get() {
    return `${firstName.value} ${lastName.value}`
  },
  set(newValue) {
    const [first, last] = newValue.split(' ')
    firstName.value = first
    lastName.value = last
  }
})
</script>

2.2 watch的高級用法

<script setup>
import { ref, reactive, watch, watchEffect } from 'vue'

const state = reactive({
  user: {
    profile: {
      name: 'Alice',
      settings: {
        theme: 'dark'
      }
    }
  },
  items: [1, 2, 3]
})

const count = ref(0)

// ==================== 基礎監聽 ====================
// 監聽ref
watch(count, (newVal, oldVal, onCleanup) => {
  console.log(`count: ${oldVal} → ${newVal}`)
  
  // 清理函數(下次執行前調用)
  onCleanup(() => {
    console.log('清理上一次的副作用')
  })
})

// 監聽reactive屬性(需要getter)
watch(
  () => state.user.profile.name,
  (newVal, oldVal) => {
    console.log(`name: ${oldVal} → ${newVal}`)
  }
)

// ==================== 監聽多個源 ====================
watch(
  [count, () => state.user.profile.name],
  ([newCount, newName], [oldCount, oldName]) => {
    console.log(`count: ${newCount}, name: ${newName}`)
  }
)

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

// ==================== 立即執行 ====================
watch(
  () => state.items,
  (newVal) => {
    console.log('items:', newVal)
  },
  {
    immediate: true  // 創建時立即執行一次
  }
)

// ==================== 監聽數組 ====================
const list = ref(['a', 'b', 'c'])

// 監聽整個數組
watch(list, (newList) => {
  console.log('list changed:', newList)
}, { deep: true })

// 監聽數組特定元素
watch(
  () => list.value[0],
  (newVal) => {
    console.log('第一項變化:', newVal)
  }
)

// ==================== 停止監聽 ====================
const stop = watch(count, () => {
  console.log('count changed')
})

// 需要時停止監聽
// stop()

// ==================== watchEffect ====================
// 自動追蹤所有依賴
watchEffect(() => {
  // 自動追蹤count和state.user.profile.name
  console.log(`count: ${count.value}, name: ${state.user.profile.name}`)
})

// watchEffect vs watch
// watchEffect:自動追蹤,立即執行
// watch:顯式指定依賴,默認不立即執行

// 帶清理的watchEffect
watchEffect((onCleanup) => {
  const controller = new AbortController()
  
  fetch(`/api/users/${count.value}`, {
    signal: controller.signal
  })
    .then(res => res.json())
    .then(data => {
      // 處理數據
    })
  
  onCleanup(() => {
    controller.abort()  // 取消未完成的請求
  })
})
</script>

2.3 響應式系統原理

Proxy代理機制

// Vue 3響應式系統的核心
const reactive = (target) => {
  return new Proxy(target, {
    get(target, key, receiver) {
      // 追蹤依賴
      track(target, key)
      return Reflect.get(target, key, receiver)
    },
    
    set(target, key, value, receiver) {
      const oldValue = target[key]
      const result = Reflect.set(target, key, value, receiver)
      if (oldValue !== value) {
        // 觸發更新
        trigger(target, key)
      }
      return result
    }
  })
}

// 依賴收集
const targetMap = new WeakMap()

function track(target, key) {
  // 在組件渲染時收集依賴
  if (activeEffect) {
    let depsMap = targetMap.get(target)
    if (!depsMap) {
      targetMap.set(target, (depsMap = new Map()))
    }
    
    let dep = depsMap.get(key)
    if (!dep) {
      depsMap.set(key, (dep = new Set()))
    }
    
    dep.add(activeEffect)
  }
}

// 觸發更新
function trigger(target, key) {
  const depsMap = targetMap.get(target)
  if (!depsMap) return
  
  const dep = depsMap.get(key)
  if (dep) {
    dep.forEach(effect => effect())
  }
}

第三步:生命週期鉤子

3.1 完整生命週期

<script setup>
import {
  onBeforeMount,
  onMounted,
  onBeforeUpdate,
  onUpdated,
  onBeforeUnmount,
  onUnmounted,
  onActivated,
  onDeactivated,
  onErrorCaptured,
  onRenderTracked,
  onRenderTriggered
} from 'vue'

// ==================== 掛載階段 ====================
onBeforeMount(() => {
  console.log('1. 組件即將掛載')
  console.log('此時DOM還未渲染')
})

onMounted(() => {
  console.log('2. 組件已掛載')
  console.log('可以訪問DOM元素、發起API請求')
  
  // 示例:獲取DOM元素
  const element = document.querySelector('#my-element')
  console.log(element)
})

// ==================== 更新階段 ====================
onBeforeUpdate(() => {
  console.log('3. 數據即將更新DOM')
  console.log('此時DOM還是舊的')
})

onUpdated(() => {
  console.log('4. DOM已更新')
  console.log('注意:避免在此修改狀態,可能導致無限循環')
})

// ==================== 卸載階段 ====================
onBeforeUnmount(() => {
  console.log('5. 組件即將卸載')
  console.log('組件實例還存在,可訪問this')
})

onUnmounted(() => {
  console.log('6. 組件已卸載')
  console.log('清理副作用:定時器、事件監聽等')
})

// ==================== KeepAlive相關 ====================
onActivated(() => {
  console.log('組件被激活(從緩存中恢復)')
})

onDeactivated(() => {
  console.log('組件被緩存(進入休眠)')
})

// ==================== 錯誤處理 ====================
onErrorCaptured((err, instance, info) => {
  console.error('捕獲錯誤:', err)
  console.error('組件實例:', instance)
  console.error('錯誤來源:', info)
  
  // 返回false阻止錯誤繼續傳播
  return false
})

// ==================== 調試鉤子 ====================
onRenderTracked((e) => {
  console.log('渲染追蹤:', e)
})

onRenderTriggered((e) => {
  console.log('渲染觸發:', e)
})
</script>

3.2 Options API到Composition API映射

Options APIComposition API
beforeCreate使用 setup()
created使用 setup()
beforeMountonBeforeMount
mountedonMounted
beforeUpdateonBeforeUpdate
updatedonUpdated
beforeUnmountonBeforeUnmount
unmountedonUnmounted
activatedonActivated
deactivatedonDeactivated

第四步:Composables(可復用邏輯)

4.1 Composable設計原則

最佳實踐

// composables/useUser.js
import { ref, computed, readonly } from 'vue'

export function useUser(userId) {
  // 1. 內部狀態(私有)
  const user = ref(null)
  const loading = ref(false)
  const error = ref(null)
  
  // 2. 計算屬性
  const fullName = computed(() => 
    user.value 
      ? `${user.value.firstName} ${user.value.lastName}`
      : ''
  )
  
  // 3. 方法
  async function fetchUser(id) {
    loading.value = true
    error.value = null
    
    try {
      const response = await fetch(`/api/users/${id}`)
      if (!response.ok) throw new Error('User not found')
      user.value = await response.json()
    } catch (e) {
      error.value = e.message
    } finally {
      loading.value = false
    }
  }
  
  function updateUser(updates) {
    if (user.value) {
      Object.assign(user.value, updates)
    }
  }
  
  // 4. 生命週期
  if (userId) {
    fetchUser(userId)
  }
  
  // 5. 返回值
  // 暴露只讀狀態和操作方法
  return {
    // 狀態(只讀)
    user: readonly(user),
    loading: readonly(loading),
    error: readonly(error),
    
    // 計算屬性
    fullName,
    
    // 方法
    fetchUser,
    updateUser
  }
}

4.2 實用Composables集合

數據獲取Composable

// composables/useFetch.js
import { ref, watchEffect, toValue, isRef } from 'vue'

export function useFetch(url, options = {}) {
  const data = ref(null)
  const error = ref(null)
  const loading = ref(false)
  
  // 支持ref/reactive參數
  const fetchData = async () => {
    loading.value = true
    error.value = null
    
    try {
      const resolvedUrl = toValue(url)  // 解包ref
      const response = await fetch(resolvedUrl, options)
      
      if (!response.ok) {
        throw new Error(`HTTP error! status: ${response.status}`)
      }
      
      data.value = await response.json()
    } catch (e) {
      error.value = e
    } finally {
      loading.value = false
    }
  }
  
  // 自動重新獲取(當URL變化時)
  watchEffect(() => {
    fetchData()
  })
  
  return {
    data,
    error,
    loading,
    refetch: fetchData
  }
}

// 使用
const { data, loading, error } = useFetch('/api/users')
const { data: user } = useFetch(() => `/api/users/${userId.value}`)  // 響應式URL

本地存儲Composable

// composables/useLocalStorage.js
import { ref, watch, readableref } from 'vue'

export function useLocalStorage(key, defaultValue, options = {}) {
  const {
    serialize = JSON.stringify,
    deserialize = JSON.parse,
    writeImmediately = true
  } = options
  
  // 從localStorage讀取初始值
  const stored = localStorage.getItem(key)
  const data = ref(
    stored ? deserialize(stored) : defaultValue
  )
  
  // 監聽變化並同步到localStorage
  watch(
    data,
    (newValue) => {
      if (writeImmediately) {
        localStorage.setItem(key, serialize(newValue))
      }
    },
    { deep: true }
  )
  
  // 手動保存(用於writeImmediately: false)
  function save() {
    localStorage.setItem(key, serialize(data.value))
  }
  
  // 清除
  function remove() {
    localStorage.removeItem(key)
    data.value = defaultValue
  }
  
  return {
    data,
    save,
    remove
  }
}

// 使用
const { data: theme } = useLocalStorage('theme', 'light')
const { data: user } = useLocalStorage('user', null)

防抖/節流Composable

// composables/useDebounce.js
import { ref, watch, unref } from 'vue'

export function useDebounce(value, delay = 300) {
  const debouncedValue = ref(unref(value))
  let timeout
  
  watch(
    value,
    (newValue) => {
      clearTimeout(timeout)
      timeout = setTimeout(() => {
        debouncedValue.value = newValue
      }, delay)
    },
    { immediate: true }
  )
  
  return debouncedValue
}

export function useThrottle(value, delay = 300) {
  const throttledValue = ref(unref(value))
  let lastExecuted = 0
  
  watch(value, (newValue) => {
    const now = Date.now()
    if (now - lastExecuted >= delay) {
      throttledValue.value = newValue
      lastExecuted = now
    }
  })
  
  return throttledValue
}

// 使用
const searchQuery = ref('')
const debouncedQuery = useDebounce(searchQuery, 500)

watch(debouncedQuery, (query) => {
  // 搜索API調用
  fetchResults(query)
})

鼠標追蹤Composable

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

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

// 使用
const { x, y } = useMouse()
console.log(`鼠標位置: ${x.value}, ${y.value}`)

窗口大小Composable

// composables/useWindowSize.js
import { ref, onMounted, onUnmounted } from 'vue'

export function useWindowSize() {
  const width = ref(window.innerWidth)
  const height = ref(window.innerHeight)
  
  function update() {
    width.value = window.innerWidth
    height.value = window.innerHeight
  }
  
  onMounted(() => {
    window.addEventListener('resize', update)
  })
  
  onUnmounted(() => {
    window.removeEventListener('resize', update)
  })
  
  return { width, height }
}

異步狀態Composable

// composables/useAsync.js
import { ref, computed } from 'vue'

export function useAsync(asyncFunction, immediate = true) {
  const data = ref(null)
  const loading = ref(false)
  const error = ref(null)
  
  const isSuccess = computed(() => !loading.value && !error.value)
  const isError = computed(() => !loading.value && error.value)
  
  async function execute(...args) {
    loading.value = true
    error.value = null
    
    try {
      data.value = await asyncFunction(...args)
    } catch (e) {
      error.value = e
    } finally {
      loading.value = false
    }
  }
  
  if (immediate) {
    execute()
  }
  
  return {
    data,
    loading,
    error,
    isSuccess,
    isError,
    execute
  }
}

// 使用
const { data, loading, error, execute } = useAsync(
  () => fetch('/api/users').then(r => r.json()),
  false  // 不立即執行
)

// 手動觸發
await execute()

第五步:依賴注入

5.1 provide/inject完整用法

<!-- 父組件 - App.vue -->
<script setup>
import { provide, ref, readonly } from 'vue'
import ChildComponent from './ChildComponent.vue'

// 響應式主題
const theme = ref('light')

function toggleTheme() {
  theme.value = theme.value === 'light' ? 'dark' : 'light'
}

// 提供數據和操作
provide('theme', readonly(theme))
provide('toggleTheme', toggleTheme)

// 使用Symbol作為key(推薦)
import { ThemeSymbol, UserSymbol } from './symbols'

provide(ThemeSymbol, theme)

// 提供異步數據
const userPromise = fetch('/api/user').then(r => r.json())
provide(UserSymbol, userPromise)
</script>

<template>
  <div :class="theme">
    <ChildComponent />
  </div>
</template>

<!-- 子組件 - ChildComponent.vue -->
<script setup>
import { inject, computed } from 'vue'
import { ThemeSymbol, UserSymbol } from './symbols'

// 注入數據
const theme = inject(ThemeSymbol, 'light')  // 第二個參數是默認值
const toggleTheme = inject('toggleTheme')

// 注入Promise(需要Suspense邊界)
const userPromise = inject(UserSymbol)
// const user = await userPromise  // 需要async setup

// 計算屬性
const isDark = computed(() => theme.value === 'dark')
</script>

<template>
  <div :class="['child', theme]">
    <p>當前主題: {{ theme }}</p>
    <button @click="toggleTheme">
      切換為{{ isDark ? '淺色' : '深色' }}主題
    </button>
  </div>
</template>

5.2 Symbol管理最佳實踐

// symbols/index.js
export const ThemeSymbol = Symbol('theme')
export const UserSymbol = Symbol('user')
export const LocaleSymbol = Symbol('locale')
export const AuthSymbol = Symbol('auth')

// 提供類型安全的注入
import { InjectionKey } from 'vue'

interface Theme {
  mode: 'light' | 'dark'
  toggle: () => void
}

export const ThemeKey: InjectionKey<Theme> = Symbol('theme')

// 使用
provide(ThemeKey, { mode: theme, toggle: toggleTheme })
const theme = inject(ThemeKey)!  // TypeScript推斷正確類型

最佳實踐

代碼組織結構

<script setup>
// ==================== 1. 導入 ====================
import { ref, computed, watch, onMounted } from 'vue'
import { useRouter, useRoute } from 'vue-router'
import { useStore } from 'vuex'
import { useFetch, useLocalStorage } from '@/composables'
import ChildComponent from './ChildComponent.vue'

// ==================== 2. Props和Emits ====================
const props = defineProps({
  userId: {
    type: Number,
    required: true
  },
  initialData: {
    type: Object,
    default: () => ({})
  }
})

const emit = defineEmits(['update', 'delete', 'error'])

// ==================== 3. 響應式狀態 ====================
const loading = ref(false)
const user = ref(null)
const error = ref(null)

// ==================== 4. 計算屬性 ====================
const fullName = computed(() => 
  user.value 
    ? `${user.value.firstName} ${user.value.lastName}`
    : ''
)

const isAuthenticated = computed(() => 
  user.value !== null
)

// ==================== 5. 方法 ====================
async function fetchUser() {
  loading.value = true
  error.value = null
  
  try {
    const response = await fetch(`/api/users/${props.userId}`)
    user.value = await response.json()
    emit('update', user.value)
  } catch (e) {
    error.value = e
    emit('error', e)
  } finally {
    loading.value = false
  }
}

function handleDelete() {
  emit('delete', props.userId)
}

// ==================== 6. 監聽器 ====================
watch(() => props.userId, fetchUser, { immediate: true })

watch(error, (newError) => {
  if (newError) {
    console.error('User fetch error:', newError)
  }
})

// ==================== 7. 生命週期 ====================
onMounted(() => {
  console.log('Component mounted')
})

// ==================== 8. 暴露給父組件 ====================
defineExpose({
  fetchUser,
  user
})
</script>

<template>
  <!-- 模板 -->
</template>

命名規範

類型命名規範示例
Composableuse前綴useUser, useFetch
ref變量描述性名稱isLoading, hasError
方法動詞開頭fetchUser, handleSubmit
事件處理handle前綴handleClick, handleSubmit
Props駝峰命名userId, initialData
Emits動詞/事件名update, delete, error

總結

Composition API核心要點:

概念用途關鍵API
響應式創建響應式數據ref, reactive, computed
監聽數據變化副作用watch, watchEffect
生命週期組件生命週期onMounted, onUnmounted
Composables邏輯復用自定義函數
依賴注入跨組件共享provide, inject

學習路徑

  1. ✅ 理解 refreactive
  2. ✅ 掌握 computedwatch
  3. ✅ 熟悉生命週期鉤子
  4. ✅ 學習編寫Composables
  5. ✅ 理解響應式原理
  6. ✅ 實踐大型組件重構

參考資料

💬 評論區

返回文章列表