Vue 3 组合式 API 是构建 Vue 应用的现代方式。它提供了更好的 TypeScript 支持、通过组合函数实现更好的代码复用,以及更灵活的组件组织方式。本指南涵盖了从响应式原语到 Pinia、Vue Router 4、Vitest 测试和性能优化等高级模式,帮助你构建生产级 Vue 应用。
核心要点
- 组合式 API 通过将相关逻辑组织在一起而非分散在各选项中,实现了更好的代码组织。
- 组合函数(use* 函数)以完整的 TypeScript 支持取代混入,成为主要的代码复用模式。
- Pinia 是官方状态管理方案,提供比 Vuex 更简洁的 API 和完整的 TypeScript 类型推断。
- 使用 defineProps 和 defineEmits 的 script setup 是单文件组件的推荐语法。
- shallowRef、v-memo 和 computed 缓存等性能工具可防止不必要的重新渲染。
- Vue Router 4 组合函数(useRouter、useRoute)与组合式 API 无缝集成进行导航。
- Teleport 和 Suspense 是内置组件,无需第三方库即可解决常见 UI 模式。
1. 组合式 API 与选项式 API
选项式 API 按选项类型(data、methods、computed、watch)组织代码,而组合式 API 按逻辑关注点分组。在一个 500 行的组件中,单个功能的相关逻辑可能分散在 data、computed、methods 和 watch 中。组合式 API 让你将所有相关逻辑放在一起。
两种 API 都使用相同的底层 Vue 响应式系统。组合式 API 不会废弃选项式 API。你甚至可以在增量迁移期间在同一组件中同时使用两者。关键优势是组合函数可以独立提取、测试和共享。
在实际应用中,当组件处理多个关注点时,组合式 API 优势明显。例如仪表板组件可能管理用户偏好、实时数据获取和图表渲染。使用选项式 API,这三个关注点会交织在 data、methods 和 watchers 中。使用组合式 API,每个关注点都在自己的组合函数中:usePreferences()、useRealtimeData() 和 useChartRenderer()。
// Options API (Vue 2 style)
export default {
data() {
return { count: 0, name: "Vue" };
},
computed: {
double() { return this.count * 2; }
},
methods: {
increment() { this.count++; }
}
};
// Composition API (Vue 3)
import { ref, computed } from "vue";
const count = ref(0);
const name = ref("Vue");
const double = computed(() => count.value * 2);
const increment = () => count.value++;2. 响应式系统:ref、reactive、computed、watch
Vue 3 提供多种响应式原语。使用 ref() 处理原始值(字符串、数字、布尔值),reactive() 处理对象。computed() 创建缓存的派生状态,仅在依赖变化时重新计算。watch() 和 watchEffect() 处理响应式数据变化时的副作用。
watch 和 watchEffect 的关键区别在于依赖追踪。watch() 需要你明确指定源并提供旧值和新值。watchEffect() 自动追踪回调中使用的所有响应式依赖,更简单但不那么精确。
toRef() 和 toRefs() 是解构响应式对象而不丢失响应性的重要工具。toRef 创建链接到 reactive 对象特定属性的 ref,而 toRefs 将所有属性转换为独立的 ref。这在从组合函数返回状态时特别有用。
import { ref, reactive, computed,
watch, watchEffect } from "vue";
// ref: wraps primitives (access via .value)
const count = ref(0);
count.value++; // 1
// reactive: deep reactive object (no .value)
const state = reactive({ items: [], loading: false });
state.loading = true;
// computed: cached derived state
const total = computed(() => state.items.length);
// watch: react to specific source changes
watch(count, (newVal, oldVal) => {
console.log(`Changed: \${oldVal} -> \${newVal}`);
});
// watchEffect: auto-tracks dependencies
watchEffect(() => {
console.log("Count:", count.value);
});3. 组合函数:可复用逻辑
组合函数是以 "use" 为前缀的函数,使用组合式 API 封装和复用有状态逻辑。它们以更干净、类型安全的模式替代 Vue 2 的混入,避免命名冲突。每个组合函数都是独立的函数,具有明确的输入和输出。
常见的组合函数模式包括 useFetch 用于数据获取、useLocalStorage 用于持久化状态、useEventListener 用于 DOM 事件、useMediaQuery 用于响应式设计。VueUse 库提供了 200 多个生产就绪的组合函数。
构建组合函数时,遵循清理模式:如果组合函数设置了事件监听器、定时器或订阅,使用 onUnmounted 进行清理。使用 Vue 3.3 的 toValue() 接受 ref 或 getter 参数作为响应式输入,它统一处理 ref 值和 getter 函数。
// composables/useFetch.ts
import { ref, watchEffect } from "vue";
export function useFetch<T>(url: string) {
const data = ref<T | null>(null);
const error = ref<string | null>(null);
const loading = ref(true);
watchEffect(async () => {
loading.value = true;
try {
const res = await fetch(url);
data.value = await res.json();
} catch (e: any) {
error.value = e.message;
} finally {
loading.value = false;
}
});
return { data, error, loading };
}4. Props 与 Emits:defineProps 和 defineEmits
script setup 提供编译器宏 defineProps 和 defineEmits,实现类型安全的组件通信。这些宏在构建时编译掉,无需导入。withDefaults 在保持完整类型推断的同时设置默认属性值。
Vue 3.3 引入了 defineModel 宏用于双向绑定,简化了 v-model 模式。无需手动声明 prop 和 emit,defineModel 创建一个自动与父组件 v-model 值同步的 ref。
对于具有大量 props 的复杂组件,考虑将相关 props 分组到接口中并导入。这使 props 定义保持干净且可在多个组件间复用。defineSlots 宏提供类型安全的作用域插槽,实现插槽内容的完整 TypeScript 检查。
<script setup lang="ts">
// defineProps with TypeScript types
interface Props {
title: string;
count?: number;
items: string[];
}
const props = withDefaults(defineProps<Props>(), {
count: 0,
items: () => [],
});
// defineEmits with typed events
const emit = defineEmits<{
update: [value: string];
delete: [id: number];
submit: [];
}>();
emit("update", "new value");
emit("delete", 42);
</script>5. Provide/Inject:依赖注入
provide 和 inject 允许在组件树中传递数据而无需逐层传递 props。使用 TypeScript 的 InjectionKey 可在提供者和消费者之间实现完整的类型安全。此模式特别适用于插件、主题系统和共享服务。
与 props 不同,提供的值默认不是响应式的。要使其响应式,提供 ref 或 reactive 对象。注入组件将自动追踪变化。你也可以提供函数让子组件更新共享状态。
常见模式是为每个上下文创建 useProvide/useInject 对。提供者组合函数调用 provide(),消费者组合函数调用 inject() 并进行适当的错误处理。这封装了注入键和类型,使消费者只需调用 useTheme() 或 useAuth() 即可。
// types/injection-keys.ts
import type { InjectionKey, Ref } from "vue";
export interface UserContext {
name: Ref<string>;
logout: () => void;
}
export const UserKey: InjectionKey<UserContext>
= Symbol("UserContext");
// ParentComponent.vue
import { provide, ref } from "vue";
import { UserKey } from "./types/injection-keys";
const name = ref("Alice");
provide(UserKey, {
name,
logout: () => { /* clear session */ }
});
// ChildComponent.vue (any depth)
import { inject } from "vue";
import { UserKey } from "./types/injection-keys";
const user = inject(UserKey); // fully typed6. Vue Router 4:组合式 API 集成
Vue Router 4 提供 useRouter() 和 useRoute() 等组合函数在 setup() 中进行导航和路由访问。导航守卫可通过 onBeforeRouteLeave 和 onBeforeRouteUpdate 在组件级别定义。
使用懒加载的路由级代码分割对性能至关重要。在路由定义中使用动态导入将每个页面分割为单独的块。结合预取,确保快速初始加载的同时保持导航即时性。
Vue Router 4 通过 RouteRecordRaw 接口支持类型化的路由定义。将路由定义在单独的文件中以更好地组织代码,使用路由 meta 字段进行认证守卫、面包屑或页面标题。全局 beforeEach 守卫检查 meta.requiresAuth 来保护路由。
import { useRouter, useRoute,
onBeforeRouteLeave } from "vue-router";
const router = useRouter();
const route = useRoute();
// Programmatic navigation
router.push({ name: "user", params: { id: "123" } });
router.replace("/dashboard");
// Access route params reactively
watch(() => route.params.id, (newId) => {
fetchUser(newId as string);
});
// Per-component navigation guard
onBeforeRouteLeave((to, from) => {
if (hasUnsavedChanges.value) {
return confirm("Discard unsaved changes?");
}
});7. Pinia 状态管理
Pinia 是 Vue 官方状态管理库,替代 Vuex。它通过 defineStore 提供简洁的 API,同时支持选项式和 setup 语法,无需额外配置即可提供完整的 TypeScript 类型推断。
Pinia store 自动支持 tree-shaking,未使用的 store 会从生产包中排除。Store 可以相互订阅,支持用于持久化或日志记录的插件,并与 Vue DevTools 集成进行时间旅行调试。
Pinia 开箱即用支持服务端渲染。在 SSR 应用中,每个请求获得自己的 Pinia 实例以避免用户之间的状态泄露。使用 storeToRefs() 辅助函数解构 store 状态同时保持响应式,类似于 reactive 对象的 toRefs()。
import { defineStore } from "pinia";
import { ref, computed } from "vue";
// Setup store syntax (recommended)
export const useCartStore = defineStore("cart", () => {
const items = ref<CartItem[]>([]);
const total = computed(() =>
items.value.reduce((s, i) => s + i.price * i.qty, 0)
);
function addItem(product: Product) {
const existing = items.value.find(
i => i.id === product.id
);
if (existing) existing.qty++;
else items.value.push({ ...product, qty: 1 });
}
function clear() { items.value = []; }
return { items, total, addItem, clear };
});8. Script Setup:defineExpose、defineOptions、顶层 await
script setup 是语法糖,将整个脚本块编译为 setup() 函数。顶层声明的变量、函数和导入自动在模板中可用,无需显式 return 语句。
使用模板 ref 调用子组件方法时 defineExpose 至关重要。没有它,script setup 中的内容对父组件不可访问。defineOptions 设置组件名称和 inheritAttrs 等选项。
从选项式 API 迁移时,注意 script setup 组件不再有 this 上下文。使用模板 ref 变量替代 this.$refs,使用 defineEmits 的 emit 函数替代 this.$emit。Vue DevTools 完全支持 script setup 组件,具有相同的检查功能。
<script setup lang="ts">
import { ref } from "vue";
// defineOptions: set component name, inheritAttrs
defineOptions({
name: "MyDialog",
inheritAttrs: false
});
// Component logic
const visible = ref(false);
const open = () => (visible.value = true);
const close = () => (visible.value = false);
// defineExpose: expose methods to parent via ref
defineExpose({ open, close });
// Top-level await (needs Suspense parent)
const config = await fetch("/api/config")
.then(r => r.json());
</script>9. Teleport 与 Suspense
Teleport 将插槽内容渲染到 CSS 选择器指定的不同 DOM 位置,解决模态框和工具提示常见的 z-index 和 overflow 问题。Suspense 提供了处理组件树中异步依赖的方式,内置加载状态。
Suspense 可以同时处理多个异步依赖。如果组件树有三个异步子组件,Suspense 等待所有子组件完成后才从回退切换到默认内容。你也可以嵌套 Suspense 边界实现更精细的加载状态。
Teleport 支持 disabled 属性,可条件性地将内容保留在原始位置。这对于响应式设计很有用,模态框仅在大屏幕上才进行 teleport。你也可以通过使用多个具有不同 to 属性的 Teleport 组件来传送到多个目标。
<!-- Teleport: render modal at document body -->
<template>
<button @click="showModal = true">Open</button>
<Teleport to="body">
<div v-if="showModal" class="modal-overlay">
<div class="modal-content">
<slot />
<button @click="showModal = false">
Close
</button>
</div>
</div>
</Teleport>
</template>
<!-- Suspense: async component with fallback -->
<Suspense>
<template #default><AsyncDashboard /></template>
<template #fallback>
<div>Loading dashboard...</div>
</template>
</Suspense>10. 自定义指令
当需要模板之外的直接 DOM 操作时,自定义指令提供底层 DOM 访问。Vue 3 中指令使用与组件生命周期对应的钩子:created、beforeMount、mounted、beforeUpdate、updated、beforeUnmount 和 unmounted。
指令接收三个参数:DOM 元素、包含值、参数和修饰符的绑定对象,以及虚拟节点。对于只需要 mounted 和 updated 且行为相同的简单情况,可以使用简写函数语法。
在 script setup 中,任何以 "v" 开头的驼峰式变量自动作为自定义指令在模板中可用。例如声明 const vFocus = { mounted: el => el.focus() } 就可以使用 v-focus,无需任何注册步骤。全局指令应在 app 实例上注册。
// directives/vFocus.ts
import type { Directive } from "vue";
export const vFocus: Directive<HTMLInputElement> = {
mounted(el) { el.focus(); }
};
// directives/vIntersection.ts
export const vIntersection: Directive<
HTMLElement, () => void
> = {
mounted(el, binding) {
const observer = new IntersectionObserver(
([entry]) => {
if (entry.isIntersecting) binding.value();
},
{ threshold: 0.1 }
);
observer.observe(el);
(el as any).__ob = observer;
},
unmounted(el) {
(el as any).__ob?.disconnect();
}
};11. 过渡与动画
Vue 提供内置的 Transition 和 TransitionGroup 组件,在元素进入、离开或在 DOM 中移动时应用 CSS 和 JavaScript 动画。它们在过渡生命周期的特定阶段自动应用 CSS 类。
TransitionGroup 渲染实际元素(由 tag 属性指定),使用 FLIP 动画技术应用移动过渡。这实现了流畅的列表重排动画。JavaScript 钩子允许集成 GSAP 等动画库。
对于复杂的多步骤动画,在 Transition 上使用 mode 属性。out-in 模式等待当前元素离开后新元素再进入,防止布局闪烁。in-out 模式相反。不设置 mode 时,两个过渡同时进行,可能导致视觉重叠。
<!-- CSS Transition -->
<Transition name="fade">
<p v-if="show">Hello</p>
</Transition>
<style>
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.3s ease;
}
.fade-enter-from,
.fade-leave-to {
opacity: 0;
}
</style>
<!-- TransitionGroup for lists -->
<TransitionGroup name="list" tag="ul">
<li v-for="item in items" :key="item.id">
{{ item.text }}
</li>
</TransitionGroup>12. 使用 Vitest 测试
Vitest 是 Vue 3 项目推荐的测试框架,基于 Vite 构建,实现即时测试执行。它与 Vue Test Utils 无缝集成进行组件测试,支持快照测试、模拟和代码覆盖率。
对于端到端测试,将 Vitest 单元测试与 Playwright 或 Cypress 配对。Vitest 处理组件和组合函数测试,E2E 工具验证完整的用户旅程。使用 Vitest 覆盖率报告识别应用中未测试的代码路径。
使用 vi.mock() 模拟测试中的 API 调用和外部依赖。Vitest 支持定时器模拟(vi.useFakeTimers)用于测试防抖组合函数,以及通过 happy-dom 或 jsdom 模拟 DOM 环境。始终测试正常路径和错误状态。
import { describe, it, expect } from "vitest";
import { mount } from "@vue/test-utils";
import Counter from "./Counter.vue";
// Component test
describe("Counter", () => {
it("increments on click", async () => {
const w = mount(Counter);
await w.find("button").trigger("click");
expect(w.text()).toContain("1");
});
it("renders with props", () => {
const w = mount(Counter, {
props: { initial: 5 }
});
expect(w.text()).toContain("5");
});
});13. 性能优化
Vue 3 提供多种工具优化渲染性能。shallowRef 避免大对象的深层响应式追踪,computed 缓存昂贵的计算并仅在依赖变化时重新求值,v-memo 在指定值未变化时跳过列表项的重新渲染。
对于有数千项的列表,使用 vue-virtual-scroller 等库实现虚拟滚动。这只渲染视口中可见的项,大幅减少 DOM 节点并提高滚动性能。结合 shallowRef 用于列表数据以避免深层响应式开销。
另一个重要优化是使用 markRaw() 处理永远不应该是响应式的大对象,如第三方类实例、大型常量数据集或 Web Worker 引用。这告诉 Vue 完全跳过响应式代理,避免对永不改变的对象进行深度观察的开销。
import { shallowRef, computed,
defineAsyncComponent } from "vue";
// shallowRef: only triggers on reassignment
const bigList = shallowRef<Item[]>([]);
// This triggers reactivity:
bigList.value = [...bigList.value, newItem];
// This does NOT trigger:
bigList.value.push(newItem);
// computed: cached, only recalculates on change
const sorted = computed(() =>
[...items.value].sort((a, b) =>
a.name.localeCompare(b.name)
)
);
// Lazy-load heavy components
const Chart = defineAsyncComponent(
() => import("./HeavyChart.vue")
);
<!-- v-memo: skip re-render if unchanged -->
<div v-for="item in list" :key="item.id"
v-memo="[item.id, item.selected]">
{{ item.name }}
</div>总结:构建现代 Vue 3 应用
组合式 API 从根本上改变了我们组织 Vue 组件的方式。通过按功能而非按选项类型分组逻辑,组件变得更易于阅读、维护和重构。关键构建块是 ref 和 reactive 用于状态,computed 用于派生值,watch/watchEffect 用于副作用。
组合函数是 Vue 3 代码复用的基石。它们提供了干净、类型安全的混入替代方案,具有明确的依赖和返回值。结合 Pinia 用于全局状态和 Vue Router 4 用于导航,组合式 API 提供了一致、可扩展的开发体验。
对于生产应用,始终从一开始就考虑性能。使用 shallowRef 处理大型数据结构,computed 处理昂贵的派生计算,v-memo 优化列表渲染,懒加载实现路由级代码分割。使用 Vitest 和 Vue Test Utils 测试组件,在应用增长时保持信心。
2026 年的 Vue 生态系统成熟且紧密连接。Pinia、Vue Router、Vitest 和 VitePress 等官方工具都围绕组合式 API 设计,从状态管理到文档提供一致的开发体验。从 script setup 开始,为共享逻辑构建组合函数,随着应用需求逐步采用高级模式。
常见问题
应该使用选项式 API 还是组合式 API?
新项目推荐使用组合式 API。它提供更好的 TypeScript 支持、通过组合函数改善代码复用,以及更灵活的代码组织方式。选项式 API 仍然受支持,适合简单组件,但组合式 API 在复杂应用中扩展性更好。
ref 和 reactive 有什么区别?
ref() 将任何值包装在通过 .value 访问的响应式引用中。reactive() 使对象深度响应式,无需 .value。原始值使用 ref,需要重新赋值整个值时也用 ref。复杂对象就地修改时使用 reactive。
组合函数与混入有什么不同?
组合函数是返回响应式状态和方法的普通函数。与混入不同,它们有明确的输入输出、无命名冲突、完整的 TypeScript 支持和清晰的数据流。它们可以组合在一起,不会出现混入的隐式合并行为。
Pinia 比 Vuex 好吗?
是的,Pinia 是 Vue 3 官方推荐的状态管理方案。它拥有更简洁的 API(无需 mutations)、完整的 TypeScript 推断、模块化设计,并支持选项式和组合式 API 模式。Vuex 已进入维护模式。
什么时候用 provide/inject,什么时候用 props?
直接的父子通信使用 props。当数据需要跳过多个层级(避免 prop 逐层传递)或插件级依赖注入时使用 provide/inject。层级较浅时始终优先使用 props。
如何测试组合函数?
在组件上下文中调用组合函数进行测试。使用 Vue Test Utils 的辅助工具或创建简单包装组件。对于没有生命周期钩子的组合函数,可直接作为函数测试。
什么是 script setup,应该使用它吗?
script setup 是减少单文件组件样板代码的编译时语法。变量和导入自动在模板中可用。它是所有使用组合式 API 的新 Vue 3 组件的推荐方式。
如何优化 Vue 3 性能?
对整体变化的大对象使用 shallowRef,对昂贵的派生计算使用 computed,用 v-memo 跳过未变化列表项的重新渲染,对数千项的列表使用虚拟滚动。避免不必要的 watcher,尽可能使用 computed。