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>
問題:
- 邏輯分散:相關代碼分散在不同選項中
- 難以維護:大型組件需要頻繁滾動
- 復用困難:mixin存在命名衝突和來源不明問題
Composition API的優勢:
<script setup>
// 用戶邏輯 - 所有相關代碼集中在一起
const { user, loading, fetchUser } = useUser()
// 購物車邏輯 - 獨立且可復用
const { cart, total, addToCart } = useCart()
// 搜索邏輯 - 清晰的邏輯邊界
const { query, results, search } = useSearch()
</script>
兩種API對比
| 維度 | Options API | Composition 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):
- 響應式數據變化時執行
- 包括
watchEffect和watch - 用於數據同步、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) | ref | reactive不支持 |
| 需要重新賦值整個對象 | 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 API | Composition API |
|---|---|
beforeCreate | 使用 setup() |
created | 使用 setup() |
beforeMount | onBeforeMount |
mounted | onMounted |
beforeUpdate | onBeforeUpdate |
updated | onUpdated |
beforeUnmount | onBeforeUnmount |
unmounted | onUnmounted |
activated | onActivated |
deactivated | onDeactivated |
第四步: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>
命名規範
| 類型 | 命名規範 | 示例 |
|---|---|---|
| Composable | use前綴 | 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 |
學習路徑:
- ✅ 理解
ref和reactive - ✅ 掌握
computed和watch - ✅ 熟悉生命週期鉤子
- ✅ 學習編寫Composables
- ✅ 理解響應式原理
- ✅ 實踐大型組件重構
💬 評論區