引言
Vue3 的 Composition API 是 Vue
框架自诞生以来最重大的范式变革。它借鉴了 React Hooks
的组合思想,同时保留了 Vue
特有的响应式系统优势。本文将从响应式系统的底层原理讲起,系统梳理
ref、reactive、computed 等核心
API,深入探讨 composables 设计模式,并与 Options API
进行全方位对比。
响应式系统核心原理
Proxy-Based Reactivity
Vue3 的响应式系统建立在 ES6 Proxy 之上,替代了 Vue2 中的
Object.defineProperty。这带来了对属性的新增/删除、数组索引修改等场景的完整支持。
graph TB
A[reactive/ref 创建响应式数据] --> B[Proxy Handler]
B --> C{操作类型}
C -->|get| D[track: 收集依赖]
C -->|set| E[trigger: 触发更新]
D --> F[activeEffect 当前正在执行的副作用]
F --> G[targetMap: WeakMap 存储依赖关系]
E --> G
G --> H[执行关联的 effect / computed / watch]
style A fill:#42b883,color:#fff
style D fill:#3498db,color:#fff
style E fill:#e74c3c,color:#fff
style G fill:#f39c12,color:#fff
依赖收集与触发的简化模型
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 const targetMap = new WeakMap ();let activeEffect = null ;function track (target, key ) { if (!activeEffect) return ; 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 ()); } }function reactive (obj ) { return new Proxy (obj, { get (target, key, receiver ) { track (target, key); const result = Reflect .get (target, key, receiver); return typeof result === 'object' && result !== null ? reactive (result) : result; }, 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; }, }); }
ref 与 reactive 的选择
ref:包装原始值
ref 通过 .value
属性包装原始类型值,使其具备响应式能力:
1 2 3 4 5 6 7 8 9 10 11 12 13 import { ref, watch } from 'vue' ;const count = ref (0 );console .log (count.value ); count.value ++; const user = ref ({ name : 'Alice' , age : 25 }); user.value .name = 'Bob' ;
reactive:代理对象
reactive 直接返回对象的 Proxy,无需
.value,但有限制:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 import { reactive } from 'vue' ;const state = reactive ({ user : { name : 'Alice' , age : 25 }, todos : [], settings : { theme : 'dark' }, }); state.user .name = 'Bob' ; state.todos .push ({ text : 'Learn Vue3' }); const { user } = state;
选择指南
flowchart TD
A[需要响应式数据] --> B{数据类型?}
B -->|原始类型 string/number/boolean| C[使用 ref]
B -->|对象/数组| D{是否需要整体替换?}
D -->|是| E[使用 ref]
D -->|否| F{是否需要解构?}
F -->|是| G[使用 ref 或 toRefs]
F -->|否| H[使用 reactive]
style C fill:#42b883,color:#fff
style E fill:#42b883,color:#fff
style G fill:#f39c12,color:#000
style H fill:#3498db,color:#fff
setup 函数与
<script setup>
传统 setup 函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 import { ref, computed, onMounted } from 'vue' ;export default { props : { initialCount : { type : Number , default : 0 }, }, setup (props, { emit, slots, attrs } ) { const count = ref (props.initialCount ); const doubled = computed (() => count.value * 2 ); function increment ( ) { count.value ++; emit ('update' , count.value ); } onMounted (() => { console .log ('Component mounted' ); }); return { count, doubled, increment }; }, };
<script setup>
语法糖(推荐)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 <script setup> import { ref, computed, onMounted } from 'vue'; const props = defineProps({ initialCount: { type: Number, default: 0 }, }); const emit = defineEmits(['update']); const count = ref(props.initialCount); const doubled = computed(() => count.value * 2); function increment() { count.value++; emit('update', count.value); } onMounted(() => { console.log('Component mounted'); }); // Everything declared is automatically available in template </script> <template> <div> <p>Count: {{ count }}, Doubled: {{ doubled }}</p> <button @click="increment">+1</button> </div> </template>
<script setup> 的优势:更少的样板代码、更好的
TypeScript 类型推断、更高的编译优化空间。
生命周期 Hooks
graph TD
A[setup / 创建阶段] --> B[onBeforeMount]
B --> C[onMounted]
C --> D{数据变化?}
D -->|是| E[onBeforeUpdate]
E --> F[onUpdated]
F --> D
D -->|卸载| G[onBeforeUnmount]
G --> H[onUnmounted]
style A fill:#42b883,color:#fff
style C fill:#3498db,color:#fff
style H fill:#e74c3c,color:#fff
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 import { onBeforeMount, onMounted, onBeforeUpdate, onUpdated, onBeforeUnmount, onUnmounted, onActivated, onDeactivated, onErrorCaptured, } from 'vue' ;onMounted (() => { console .log ('First mounted callback' ); });onMounted (() => { console .log ('Second mounted callback — both will execute' ); });
provide / inject 依赖注入
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 import { provide, ref } from 'vue' ;const theme = ref ('dark' );const toggleTheme = ( ) => { theme.value = theme.value === 'dark' ? 'light' : 'dark' ; };const THEME_KEY = Symbol ('theme' );provide (THEME_KEY , { theme, toggleTheme });import { inject } from 'vue' ;const { theme, toggleTheme } = inject (THEME_KEY , { theme : ref ('light' ), toggleTheme : () => {}, });
TypeScript 类型安全的 provide/inject:
1 2 3 4 5 6 7 8 9 10 11 12 import type { InjectionKey , Ref } from 'vue' ;interface ThemeContext { theme : Ref <'light' | 'dark' >; toggleTheme : () => void ; }export const THEME_KEY : InjectionKey <ThemeContext > = Symbol ('theme' );provide (THEME_KEY , { theme, toggleTheme });const themeCtx = inject (THEME_KEY )!;
Composables 设计模式
Composables 是 Composition API 最核心的复用机制——以 use
开头的函数,封装并复用有状态逻辑。
模式一:useMouse —
全局事件监听
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 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 }; }
模式二:useAsync —
异步操作封装
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 import { ref, shallowRef } from 'vue' ;export function useAsync (asyncFn ) { const data = shallowRef (null ); const error = ref (null ); const loading = ref (false ); async function execute (...args ) { loading.value = true ; error.value = null ; try { data.value = await asyncFn (...args); } catch (err) { error.value = err; } finally { loading.value = false ; } } return { data, error, loading, execute }; }const { data : users, loading, execute : fetchUsers } = useAsync ( () => fetch ('/api/users' ).then (r => r.json ()) );onMounted (() => fetchUsers ());
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 import { reactive, computed } from 'vue' ;export function useFormValidation (rules ) { const errors = reactive ({}); const fields = reactive ({}); function setField (name, value ) { fields[name] = value; validateField (name); } function validateField (name ) { const fieldRules = rules[name]; if (!fieldRules) return ; const fieldErrors = []; for (const rule of fieldRules) { const result = rule.validator (fields[name], fields); if (!result) { fieldErrors.push (rule.message ); } } errors[name] = fieldErrors; } function validateAll ( ) { Object .keys (rules).forEach (validateField); return isValid.value ; } const isValid = computed (() => Object .values (errors).every (errs => errs.length === 0 ) ); return { fields, errors, setField, validateAll, isValid }; }const { fields, errors, setField, validateAll, isValid } = useFormValidation ({ email : [ { validator : v => !!v, message : '邮箱不能为空' }, { validator : v => /.+@.+\..+/ .test (v), message : '邮箱格式不正确' }, ], password : [ { validator : v => !!v, message : '密码不能为空' }, { validator : v => v?.length >= 8 , message : '密码至少8位' }, ], });
Composition API vs Options
API
graph LR
subgraph "Options API: 按选项分组"
A1[data] --> A2[methods]
A2 --> A3[computed]
A3 --> A4[watch]
A4 --> A5[lifecycle hooks]
end
subgraph "Composition API: 按功能分组"
B1[Feature A: state + logic + lifecycle]
B2[Feature B: state + logic + lifecycle]
B3[Feature C: state + logic + lifecycle]
end
style A1 fill:#e74c3c,color:#fff
style A2 fill:#3498db,color:#fff
style A3 fill:#2ecc71,color:#fff
style B1 fill:#9b59b6,color:#fff
style B2 fill:#f39c12,color:#000
style B3 fill:#1abc9c,color:#fff
代码组织
按选项类型分组
按功能逻辑分组
逻辑复用
Mixins(命名冲突风险)
Composables(清晰的输入输出)
TypeScript
需要额外类型体操
原生友好,类型自动推断
学习曲线
低(对象结构直观)
中(需理解响应式原语)
适用场景
简单组件
复杂组件、逻辑复用
实战:组合多个 Composables
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 <script setup> import { useFetch } from '@/composables/useFetch'; import { useDebounce } from '@/composables/useDebounce'; import { usePagination } from '@/composables/usePagination'; import { ref, computed, watch } from 'vue'; const search = ref(''); const debouncedSearch = useDebounce(search, 300); const { page, pageSize, offset, nextPage, prevPage } = usePagination(); const url = computed( () => `/api/products?q=${debouncedSearch.value}&offset=${offset.value}&limit=${pageSize.value}` ); const { data, loading, error } = useFetch(url); // Reset pagination when search changes watch(debouncedSearch, () => { page.value = 1; }); </script> <template> <div> <input v-model="search" placeholder="搜索产品..." /> <div v-if="loading">加载中...</div> <div v-else-if="error">错误: {{ error.message }}</div> <div v-else> <ProductList :products="data?.items" /> <Pagination :page="page" :total="data?.total" @next="nextPage" @prev="prevPage" /> </div> </div> </template>
总结
Vue3 Composition API 通过响应式原语(ref /
reactive / computed)和生命周期
Hooks,提供了一种灵活的代码组织方式。Composables 模式解决了 Mixins
的命名冲突和来源不明问题,使逻辑复用变得清晰可控。推荐在新项目中优先使用
<script setup> 语法配合 Composition
API,而简单组件仍可使用 Options API。两种 API 在 Vue3
中完全兼容,可以按需选择。