Vue 3 完全指南:Composition API、Pinia、Vue Router 4 和 TypeScript(2024/2025)
全面的 Vue 3 指南,涵盖 Composition API、script setup 语法、Vue Router 4、Pinia 状态管理、组合式函数、模板指令、性能优化,以及 Vue 3 vs Vue 2 vs React vs Angular 对比,附真实 TypeScript 代码示例。
TL;DR
Vue 3 是一个渐进式 JavaScript 框架,专注于可增量采用。其 Composition API(替代 Options API)实现了更好的 TypeScript 集成、通过组合式函数复用逻辑以及细粒度响应性。Vue 3 比 Vue 2 小 41%,支持 tree-shaking,并包含 Teleport、Suspense 和 defineAsyncComponent 等强大新特性。script setup 语法大幅减少了样板代码。Pinia 已取代 Vuex 成为官方状态管理方案。如果你在构建新项目,请使用 Vue 3 + script setup + TypeScript + Pinia + Vue Router 4。
核心要点
- Composition API 是编写 Vue 3 组件的推荐方式——它按功能而非生命周期钩子组织逻辑,使大型组件更易于维护和测试。
- script setup 是 Composition API 的语法糖,消除了样板代码:无需显式返回值,defineProps/defineEmits 替代选项对象,TypeScript 类型推断自动工作。
- Pinia 是官方 Vue 状态管理库——使用 Composition API,完全 TypeScript 类型化,支持 Vue DevTools,比 Vuex 更简单,没有 mutations。
- 组合式函数(useXxx 函数)是 Vue 3 中可复用有状态逻辑的模式——它们替代了 mixins,在大多数用例中提供完整的 TypeScript 类型支持。
- Vue 3 比 Vue 2 小 41%,完全支持 tree-shaking,意味着未使用的特性不会包含在生产包中。
- Vue Router 4 专为 Vue 3 设计,提供一流的 TypeScript 支持、Composition API 集成(useRoute、useRouter)和改进的导航守卫。
Vue 3 于 2020 年 9 月发布,是对 Vue 2 的全面重新设计,专注于性能、TypeScript 支持和开发者体验。由尤雨溪创建并由活跃的开源社区维护,Vue 3 驱动着阿里巴巴、小米、GitLab 以及全球数千个其他组织的应用程序。这个框架达到了独特的平衡:对初学者友好(模板语法接近纯 HTML),对高级用例强大(Composition API 在表现力上与 React hooks 相当),并且在设计上是渐进式的(可以在任何项目中增量采用)。本指南是您 2024/2025 年 Vue 3 的完整参考,涵盖从 ref 和 reactive 基础到自定义组合式函数、Pinia store 和性能优化等高级模式的所有内容。
Vue 3 是什么?渐进式框架
Vue 3 将自己描述为"渐进式 JavaScript 框架"。这意味着它被设计为可在不同规模下采用:作为独立脚本为静态 HTML 页面添加交互,或作为具有路由、状态管理和服务器端渲染的全功能 SPA 框架。与 Angular(对技术栈每一层都有强约定)或 React(只是一个渲染库,需要第三方决策)不同,Vue 占据了一个甜蜜的中间地带——它为路由(Vue Router)和状态(Pinia)提供合理的默认值和官方解决方案,同时保持灵活性。
Vue 3 相对 Vue 2 的核心改进
Vue 3 用 TypeScript 重写了整个框架,并引入了全新的虚拟 DOM 实现(受 Inferno 启发),速度显著更快。主要改进包括:包体积减少 41%、渲染速度提升最高 55%、组件初始化速度提升最高 133%、基于 Proxy 的响应性(替换 Object.defineProperty——不再需要 Vue.set() 技巧)、用于更好逻辑组织的 Composition API、Fragments(模板中的多个根元素)、用于类似 portal 渲染的 Teleport,以及用于异步组件处理的 Suspense。
Vue 3 生态系统
官方 Vue 3 生态系统包括:Vue Router 4(客户端路由)、Pinia(状态管理,Vuex 的继任者)、Vite(构建工具,由尤雨溪创建)、Vitest(单元测试)、Vue DevTools(用于调试的浏览器扩展)、VueUse(200+ 组合式工具集合)、Nuxt 3(构建在 Vue 3 上的全栈框架,类似于 React 的 Next.js),以及 Quasar/Vuetify/PrimeVue 等 UI 组件库。
Composition API 基础:ref、reactive、computed、watch
Composition API 是一组函数,允许你按功能而非生命周期钩子组织组件逻辑。在 Options API 中,单个功能(例如搜索功能)的逻辑分散在 data、methods、computed 和 watch 选项中。Composition API 将所有相关逻辑放在 setup() 函数内部,使组件更易于理解、测试,并提取为可复用的组合式函数。
ref() 和 reactive():响应式状态
ref() 为基本类型值(字符串、数字、布尔值)创建响应式引用。在 JavaScript 中通过 .value 访问和修改值。在模板中,Vue 自动解包 ref,不需要 .value。reactive() 创建响应式对象——基于 Proxy 且深度响应。对基本类型值使用 ref(),当需要重新赋整个值时也用 ref();对始终访问属性的复杂对象使用 reactive()。关键规则:在不使用 toRefs() 或 storeToRefs() 的情况下永远不要解构 reactive() 对象,因为解构会破坏响应性。
// ref() for primitives
import { ref, reactive, computed, watch, watchEffect } from 'vue';
const count = ref(0);
const message = ref('Hello Vue 3');
// Access via .value in <script>
count.value++; // 1
console.log(count.value); // 1
// In template, Vue auto-unwraps: {{ count }} not {{ count.value }}
// reactive() for objects
const user = reactive({
name: 'Alice',
age: 30,
address: { city: 'Paris' },
});
// Mutate directly (no .value needed)
user.name = 'Bob';
user.address.city = 'London'; // deep reactivity works
// WRONG: breaking reactivity by destructuring
// const { name } = user; // name is no longer reactive!
// CORRECT: use toRefs to maintain reactivity
import { toRefs } from 'vue';
const { name, age } = toRefs(user); // still reactivecomputed()、watch() 和 watchEffect()
computed() 创建一个记忆化的派生值——它自动跟踪依赖项,只在依赖项变化时重新计算。对从响应式状态派生的任何值使用它。watch() 显式监听一个或多个响应式源,并在它们变化时运行回调——默认是惰性的(不在初始渲染时运行),并接收新值和旧值。watchEffect() 立即运行其回调并自动跟踪内部访问的任何响应式依赖——非常适合应与状态同步的副作用。在 watch() 上使用 { immediate: true } 来复制 watchEffect() 的行为。
import { ref, computed, watch, watchEffect } from 'vue';
const firstName = ref('Evan');
const lastName = ref('You');
// computed: auto-tracks dependencies, memoized
const fullName = computed(() => firstName.value + ' ' + lastName.value);
console.log(fullName.value); // "Evan You"
// Writable computed
const reversedName = computed({
get: () => firstName.value.split('').reverse().join(''),
set: (val: string) => { firstName.value = val.split('').reverse().join(''); },
});
// watch: explicit deps, receives old + new value
watch(firstName, (newVal, oldVal) => {
console.log('firstName changed from ' + oldVal + ' to ' + newVal);
});
// watch multiple sources
watch([firstName, lastName], ([newFirst, newLast], [oldFirst, oldLast]) => {
console.log('Name changed');
});
// watch with immediate: true
watch(firstName, (val) => { document.title = val; }, { immediate: true });
// watchEffect: auto-tracks, runs immediately
watchEffect(() => {
console.log('firstName is now:', firstName.value); // runs on mount + changes
console.log('lastName is now:', lastName.value);
});<script setup> 语法:消除样板代码
<script setup> 语法(Vue 3.2 引入)是在单文件组件中用于 Composition API 的编译语法糖。这是编写 Vue 3 组件的推荐方式,因为它更简洁,具有更好的 TypeScript 推断,并且运行时性能更好。顶层声明的变量、函数和导入自动在模板中可用——不需要显式的 return 语句。
defineProps、defineEmits 和 defineExpose
在 <script setup> 内部,props 用 defineProps() 声明,emits 用 defineEmits() 声明——两者都是无需导入即可使用的编译器宏。使用 TypeScript 时,可以用泛型类型语法定义 props:defineProps<{ title: string; count: number }>()——这提供了完整的类型安全和 IDE 自动补全。defineEmits<{ change: [value: string]; submit: [] }>() 同样对发出的事件进行类型化。defineExpose() 显式地将组件内部暴露给父组件的模板 ref(因为 script setup 组件默认是封闭的)。
<!-- UserCard.vue -->
<script setup lang="ts">
import { ref, computed } from 'vue';
// Props with TypeScript generics — no import needed for defineProps
const props = defineProps<{
name: string;
age: number;
role?: string; // optional prop
}>();
// Default values with withDefaults
const propsWithDefaults = withDefaults(defineProps<{ role?: string }>(), {
role: 'viewer',
});
// Emits with TypeScript
const emit = defineEmits<{
select: [userId: number]; // named tuple syntax
update: [name: string, age: number];
close: []; // no payload
}>();
const isExpanded = ref(false);
const initials = computed(() =>
props.name.split(' ').map(n => n[0]).join('')
);
function handleSelect() {
emit('select', 42);
}
// defineExpose: make internal state accessible via template ref
defineExpose({ isExpanded, initials });
</script>
<template>
<div @click="handleSelect">
<span>{{ initials }}</span>
<p>{{ props.name }}, {{ props.age }}</p>
</div>
</template>useTemplateRef() 和模板 Ref
useTemplateRef()(Vue 3.5+)是获取 DOM 元素或子组件实例引用的新方式。在模板中与 ref 属性一起使用:const el = useTemplateRef("myEl")。Vue 3.5 之前,模式是 const el = ref<HTMLElement | null>(null),模板中配以 ref="el"。模板 ref 在组件挂载后填充,因此始终在 onMounted() 内部使用它们,或监视它们进行 null 检查。
<script setup lang="ts">
import { useTemplateRef, onMounted, ref } from 'vue';
// Vue 3.5+ approach
const inputEl = useTemplateRef<HTMLInputElement>('myInput');
// Pre-3.5 approach (still works)
const legacyInputEl = ref<HTMLInputElement | null>(null);
onMounted(() => {
// Populated after mount
inputEl.value?.focus();
legacyInputEl.value?.select();
});
</script>
<template>
<!-- ref attribute matches the string in useTemplateRef() -->
<input ref="myInput" type="text" placeholder="Auto-focused on mount" />
<input ref="legacyInputEl" type="text" placeholder="Legacy approach" />
</template>Vue Router 4:导航、守卫和动态路由
Vue Router 4 是 Vue 3 的官方路由器,以 TypeScript 和 Composition API 为核心重建。它为单页应用程序提供声明式、基于组件的路由。createRouter() 替代 new VueRouter(),createWebHistory() 替代 mode: "history"。通过 useRoute() 和 useRouter() 进行的 Composition API 集成替代了 this.$route / this.$router Options API 访问器。
设置 Vue Router 4
通过定义路由记录、使用 createRouter() 创建路由器并使用 app.use(router) 安装来配置 Vue Router 4。每个路由记录将路径映射到组件。history 模式(createWebHistory)使用 HTML5 History API 提供无哈希的干净 URL。hash 模式(createWebHashHistory)对不支持服务器端 URL 重写的环境使用 URL 哈希。memory 模式(createMemoryHistory)用于 SSR 和测试。
// src/router/index.ts
import { createRouter, createWebHistory, RouteRecordRaw } from 'vue-router';
const routes: RouteRecordRaw[] = [
{
path: '/',
name: 'home',
component: () => import('../views/HomeView.vue'), // lazy-loaded
},
{
path: '/users',
component: () => import('../views/UsersLayout.vue'),
children: [
{ path: '', name: 'user-list', component: () => import('../views/UserList.vue') },
{ path: ':id', name: 'user-detail', component: () => import('../views/UserDetail.vue') },
],
},
{
path: '/admin',
component: () => import('../views/AdminView.vue'),
meta: { requiresAuth: true, requiresAdmin: true },
},
{ path: '/:pathMatch(.*)*', name: 'not-found', component: () => import('../views/NotFound.vue') },
];
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes,
scrollBehavior(to, from, savedPosition) {
if (savedPosition) return savedPosition;
return { top: 0 };
},
});
export default router;导航守卫和路由元信息
导航守卫在导航确认之前运行,允许你重定向或取消。全局守卫(router.beforeEach)在每次导航时运行。路由独享守卫(路由记录上的 beforeEnter)只对特定路由运行。组件守卫(来自 Composition API 的 onBeforeRouteLeave、onBeforeRouteUpdate)在组件导航事件时运行。路由元信息字段(meta: { requiresAuth: true })存储关于路由的任意数据,通常用于在全局守卫中通过身份验证检查保护路由。
// Global navigation guard — authentication check
router.beforeEach(async (to, from) => {
const authStore = useAuthStore();
if (to.meta.requiresAuth && !authStore.isLoggedIn) {
return { name: 'login', query: { redirect: to.fullPath } };
}
if (to.meta.requiresAdmin && !authStore.isAdmin) {
return { name: 'forbidden' };
}
// return undefined or true to proceed
});
// In-component guards (Composition API)
<script setup lang="ts">
import { onBeforeRouteLeave, onBeforeRouteUpdate, useRoute, useRouter } from 'vue-router';
const route = useRoute(); // reactive current route
const router = useRouter(); // router instance for programmatic navigation
console.log(route.params.id); // current :id param
console.log(route.query.page); // ?page= query param
console.log(route.meta); // typed route meta
onBeforeRouteLeave((to, from) => {
if (hasUnsavedChanges.value) {
return confirm('You have unsaved changes. Leave?');
}
});
onBeforeRouteUpdate((to, from) => {
// fires when same component used for different params
fetchUser(to.params.id as string);
});
// Programmatic navigation
router.push({ name: 'user-detail', params: { id: 42 } });
router.replace('/dashboard');
router.go(-1); // go back
</script>动态路由、路由参数和嵌套路由
动态路由段使用冒号语法(/users/:id)。在 script setup 中通过 useRoute().params.id 访问当前参数。通配路由使用 (:path)* 或 :pathMatch(.*)*。嵌套路由通过向路由记录添加 children 数组定义——父路由为子路由渲染 <RouterView>。命名路由(name: "user-detail")配合 router.push({ name: "user-detail", params: { id: 1 } }) 比硬编码路径字符串更易维护,随着路由结构演进。
Pinia 状态管理:Vuex 的继任者
Pinia 是 Vue 3 的官方状态管理库,由 Vue 核心团队推荐取代 Vuex 4。它围绕 Composition API 设计,没有 mutations(只有 actions 和 state),开箱即用完全 TypeScript 类型化,支持 Vue DevTools 进行时间旅行调试,设计上是模块化的(没有单一大型 store 文件),并且体积极小(约 1.5KB 压缩后)。Pinia store 可以在组件、其他 store 和组件外部使用。
defineStore:创建 Pinia Store
使用 defineStore() 创建 store。第一个参数是 DevTools 使用的唯一字符串 ID。第二个参数是 Options 对象(包含 state、getters 和 actions——类似于 Vuex)或 Setup 函数(使用带有 ref、computed 和函数的 Composition API——首选方式)。两种方式都产生与 Vue DevTools 完全集成、支持插件,并可与 storeToRefs() 一起使用进行响应式解构的 store。
// src/stores/counter.ts — Setup Store (preferred)
import { defineStore } from 'pinia';
import { ref, computed } from 'vue';
export const useCounterStore = defineStore('counter', () => {
// state
const count = ref(0);
const history = ref<number[]>([]);
// getters (computed)
const doubleCount = computed(() => count.value * 2);
const isPositive = computed(() => count.value > 0);
// actions
function increment() {
history.value.push(count.value);
count.value++;
}
async function fetchAndSet(id: number) {
const data = await fetch('/api/counts/' + id).then(r => r.json());
count.value = data.value;
}
function $reset() { count.value = 0; history.value = []; }
return { count, history, doubleCount, isPositive, increment, fetchAndSet, $reset };
});
// src/stores/user.ts — Options Store style
export const useUserStore = defineStore('user', {
state: () => ({ name: '', email: '', isLoggedIn: false }),
getters: {
displayName: (state) => state.name || 'Guest',
},
actions: {
async login(email: string, password: string) {
const user = await authService.login(email, password);
this.name = user.name;
this.email = user.email;
this.isLoggedIn = true;
},
logout() {
this.$patch({ name: '', email: '', isLoggedIn: false });
},
},
});storeToRefs、Actions 和 DevTools
要在保持响应性的同时从 Pinia store 解构响应式属性,使用 storeToRefs()——它将每个 state 属性和 getter 包装在 ref 中。方法(actions)可以直接解构而无需 storeToRefs()。Pinia actions 替代了 Vuex 的 mutations 和 actions——它们可以是同步或异步的,可以访问 this(store 实例),并在 Vue DevTools 时间线中自动追踪。使用 $patch() 进行批量状态更新,无需通过 actions。
<script setup lang="ts">
import { storeToRefs } from 'pinia';
import { useCounterStore } from '@/stores/counter';
const counterStore = useCounterStore();
// storeToRefs preserves reactivity for state + getters
const { count, doubleCount, isPositive } = storeToRefs(counterStore);
// Actions can be destructured directly (they are not reactive)
const { increment, fetchAndSet } = counterStore;
// Batch update with $patch
counterStore.$patch({ count: 10 });
// $patch with mutation function for complex updates
counterStore.$patch((state) => {
state.count += 5;
state.history.push(state.count);
});
// Subscribe to store changes
counterStore.$subscribe((mutation, state) => {
console.log('Store mutated:', mutation.type, state.count);
localStorage.setItem('counter', JSON.stringify(state));
});
</script>
<template>
<p>Count: {{ count }} | Double: {{ doubleCount }}</p>
<button @click="increment">Increment</button>
</template>Vue 3 模板指令
Vue 指令是以 v- 为前缀的特殊 HTML 属性,将响应式行为应用于 DOM。它们是响应式数据和 HTML 模板之间的桥梁。Vue 3 提供内置指令,用于条件渲染、列表渲染、双向绑定、事件处理和属性绑定。自定义指令允许你将直接的 DOM 操作封装为可复用的声明式 HTML 属性。
v-if、v-else、v-show、v-for
v-if / v-else / v-else-if 条件渲染元素——当条件改变时,元素在 DOM 中被销毁并重新创建(最适合不频繁切换的元素)。v-show 切换 CSS display:none——元素保留在 DOM 中(最适合频繁切换的元素)。v-for 使用 item in items 或 (item, index) in items 语法渲染列表。始终为 v-for 提供 :key 属性以实现高效的 DOM diff——使用数据中的唯一 ID 而非数组索引,以避免重新排序或删除项目时的渲染 bug。
<template>
<!-- v-if / v-else-if / v-else: element destroyed+recreated -->
<div v-if="role === 'admin'">Admin Panel</div>
<div v-else-if="role === 'editor'">Editor Panel</div>
<div v-else>Viewer Panel</div>
<!-- v-show: always rendered, toggles display:none -->
<div v-show="isExpanded">Expandable Content</div>
<!-- v-for with :key (use unique ID, not array index) -->
<ul>
<li v-for="item in items" :key="item.id">
{{ item.name }} — ${{ item.price }}
</li>
</ul>
<!-- v-for with index -->
<ol>
<li v-for="(item, index) in items" :key="item.id">
{{ index + 1 }}. {{ item.name }}
</li>
</ol>
<!-- v-for on a range -->
<span v-for="n in 5" :key="n">{{ n }} </span>
<!-- Use <template> to group without extra DOM elements -->
<template v-for="item in items" :key="item.id">
<dt>{{ item.term }}</dt>
<dd>{{ item.definition }}</dd>
</template>
</template>v-model、v-bind、v-on
v-model 在表单输入和响应式状态之间创建双向数据绑定。在 Vue 3 中,组件上的 v-model 展开为 :modelValue="value" @update:modelValue="value = $event"——组件应接受 modelValue prop 并发出 update:modelValue。单个组件支持多个 v-model 绑定:v-model:title="title" v-model:content="content"。v-bind(简写 :)动态绑定属性或 prop。v-on(简写 @)附加事件监听器。.prevent、.stop、.once、.passive 修饰符无需样板代码即可处理常见事件场景。
<script setup lang="ts">
import { ref } from 'vue';
const inputValue = ref('');
const isChecked = ref(false);
const selected = ref('option1');
</script>
<template>
<!-- Basic v-model on input -->
<input v-model="inputValue" type="text" />
<!-- v-model modifiers -->
<input v-model.trim="inputValue" /> <!-- trims whitespace -->
<input v-model.number="price" type="number" /> <!-- coerce to number -->
<input v-model.lazy="search" /> <!-- sync on change, not input -->
<!-- v-bind shorthand: -->
<img :src="user.avatar" :alt="user.name" />
<button :disabled="isLoading">{{ isLoading ? 'Loading...' : 'Submit' }}</button>
<!-- Bind object of attributes -->
<div v-bind="{ id: 'main', class: 'container', tabindex: 0 }">...</div>
<!-- v-on shorthand @ -->
<button @click="handleClick">Click</button>
<form @submit.prevent="handleSubmit">...</form>
<input @keyup.enter="search" @keyup.escape="clearSearch" />
<div @click.self="onOverlayClick">...</div> <!-- only fires on exact element -->
<!-- v-model on custom component (Vue 3) -->
<!-- Expands to: :modelValue="title" @update:modelValue="title = $event" -->
<MyInput v-model="title" />
<!-- Multiple v-model bindings -->
<UserForm v-model:name="user.name" v-model:email="user.email" />
</template>v-slot 和组件插槽
插槽允许组件从父组件接收模板内容。默认插槽渲染放置在组件标签之间的任何内容。具名插槽(<template #header>)允许多个内容区域。作用域插槽将子组件的数据暴露给父模板——v-slot="slotProps" 接收数据。插槽是布局组件(卡片、对话框、表格)的主要组合机制——它们比基于 prop 的渲染更灵活和可组合。
组合式函数和可复用逻辑
组合式函数是使用 Vue Composition API 封装和复用有状态逻辑的函数。以 "use" 前缀命名(useCounter、useFetch、useLocalStorage),它们是 Vue 3 中 React hooks 的等价物,完全替代了 mixins。与 mixins 不同,组合式函数具有显式数据流(可以清楚地看到它们返回什么),不污染组件命名空间,可以接受和返回响应式状态,并且可以相互调用以进行组合。
useXxx 模式和 VueUse
组合式函数遵循简单模式:它是一个使用 Vue 响应性 API(ref、reactive、computed、watch、生命周期钩子)并返回响应式状态或方法的函数。组合式函数在初始化时不应有外部副作用——从组件的角度来看它们应该是纯粹的。VueUse 库(vueuse.org)提供 200+ 现成的组合式函数,用于常见浏览器 API、传感器、动画、状态和工具——useFetch、useLocalStorage、useIntersectionObserver、useDark、useWindowSize 等等。
// src/composables/useFetch.ts
import { ref, watchEffect, type Ref } from 'vue';
interface UseFetchReturn<T> {
data: Ref<T | null>;
error: Ref<Error | null>;
isLoading: Ref<boolean>;
refetch: () => void;
}
export function useFetch<T>(url: Ref<string> | string): UseFetchReturn<T> {
const data = ref<T | null>(null);
const error = ref<Error | null>(null);
const isLoading = ref(false);
let controller: AbortController;
async function fetchData() {
controller?.abort(); // cancel previous request
controller = new AbortController();
isLoading.value = true;
error.value = null;
try {
const endpoint = typeof url === 'string' ? url : url.value;
const res = await fetch(endpoint, { signal: controller.signal });
if (!res.ok) throw new Error('HTTP ' + res.status);
data.value = await res.json() as T;
} catch (e) {
if ((e as Error).name !== 'AbortError') error.value = e as Error;
} finally {
isLoading.value = false;
}
}
watchEffect(() => { fetchData(); });
return { data, error, isLoading, refetch: fetchData };
}
// Usage in component
<script setup lang="ts">
import { ref } from 'vue';
import { useFetch } from '@/composables/useFetch';
interface User { id: number; name: string; email: string; }
const userId = ref(1);
const url = computed(() => '/api/users/' + userId.value);
const { data: user, isLoading, error } = useFetch<User>(url);
</script>用于深层 prop 传递的 provide() 和 inject()
provide() 和 inject() 解决了 prop 钻取问题——通过许多组件层传递 props 只是为了到达深层嵌套的组件。父组件调用 provide("key", value) 使值对所有后代可用。任何后代调用 inject("key") 来接收它。在 Vue 3 和 TypeScript 中,使用 Vue 的 InjectionKey<T> 创建的类型化注入键以获得完整类型安全。Pinia store 消除了大多数共享状态使用 provide/inject 的需求,但它对组件库 API 仍然有用(例如,表单组件向其输入提供验证上下文)。
Vue 3 性能:Teleport、Suspense 和异步组件
Vue 3 包含几个超越更快虚拟 DOM 的内置性能特性。编译器执行静态分析并提升静态虚拟节点,在重新渲染期间完全跳过它们。它还使用补丁标志标记动态绑定,允许运行时跳过对静态部分的 diff。这些编译器优化自动发生——你只需编写普通的 Vue 模板。
Teleport:在组件树外渲染
<Teleport> 组件在 DOM 中与在组件树中声明的位置不同的地方渲染其内容。使用它在 document.body 级别渲染模态框、提示和工具提示(避免祖先 CSS 造成的 z-index 和 overflow 问题),同时保持模态框逻辑与控制它的组件协同定位。to prop 接受 CSS 选择器或 DOM 元素。多个 Teleport 可以定位同一目标——它们按顺序追加。
<!-- Modal component using Teleport -->
<script setup lang="ts">
defineProps<{ isOpen: boolean; title: string }>();
defineEmits<{ close: [] }>();
</script>
<template>
<!-- Renders at document.body, not in component tree -->
<Teleport to="body">
<div v-if="isOpen" class="modal-overlay" @click.self="$emit('close')">
<div class="modal">
<h2>{{ title }}</h2>
<slot />
<button @click="$emit('close')">Close</button>
</div>
</div>
</Teleport>
</template>
<!-- Suspense + defineAsyncComponent -->
<script setup lang="ts">
import { defineAsyncComponent } from 'vue';
const HeavyChart = defineAsyncComponent({
loader: () => import('./HeavyChart.vue'),
loadingComponent: LoadingSpinner,
errorComponent: ErrorDisplay,
delay: 200, // show loading after 200ms
timeout: 10000, // error after 10s
});
</script>
<template>
<!-- Suspense with fallback slot -->
<Suspense>
<template #default>
<HeavyChart :data="chartData" />
</template>
<template #fallback>
<div>Loading chart...</div>
</template>
</Suspense>
</template>Suspense 和异步组件
<Suspense> 组件处理组件树中的异步依赖,在异步操作完成时显示 fallback 插槽。defineAsyncComponent() 按需懒加载组件,将它们拆分为单独的包。与 <Suspense> 一起使用时,父组件可以在多个异步组件同时加载时显示单一加载状态。此模式消除了分散的 v-if="loading" 检查,并提供了干净的 loading/error 边界模型(类似于 React Suspense/Error Boundaries)。
Vue 3 Tree-Shaking 和包优化
Vue 3 完全支持 tree-shaking——内置 API(Transition、KeepAlive、Teleport)按需导入而非始终包含。如果你的应用不使用 Teleport,它就不在你的包中。这就是为什么 Vue 3 的基准包比 Vue 2 小 41%。使用 Vite(推荐的构建工具),开发构建使用原生 ESM 实现即时热模块替换,生产构建使用 Rollup 进行激进的 tree-shaking 和代码分割。使用 defineAsyncComponent() 和路由级代码分割进一步减小初始包大小。
Vue 3 vs Vue 2 vs React vs Angular 对比
选择正确的框架取决于团队专业知识、项目规模和生态系统需求。以下是 Vue 3、Vue 2、React 和 Angular 在关键维度上的全面对比。
| 维度 | Vue 3 | Vue 2 | React 18 | Angular 17 |
|---|---|---|---|---|
| 类型 | 渐进式框架 | 渐进式框架 | UI 库 | 完整框架 |
| TypeScript | 优秀(内置) | 一般(需配置) | 良好(JSX) | 原生强制 |
| 学习曲线 | 低–中 | 低 | 中 | 高 |
| 状态管理 | Pinia(官方) | Vuex(官方) | Redux / Zustand / Jotai | NgRx / Signals |
| 路由 | Vue Router 4(官方) | Vue Router 3(官方) | React Router / TanStack | @angular/router(内置) |
| 全栈框架 | Nuxt 3 | Nuxt 2 | Next.js | Angular Universal |
| 包大小(基准) | ~22KB gzipped | ~33KB gzipped | ~42KB gzipped | ~75KB gzipped |
| 性能 | 极快 | 快 | 快(Concurrent) | 好(Signals) |
| 模板语法 | HTML 模板 + JSX | HTML 模板 | JSX(必须) | HTML 模板(Angular 特有) |
| 企业采用 | 中(阿里巴巴、小米) | 中(遗留) | 极高(Meta、Netflix) | 高(Google、企业) |
| 生命周期 | 活跃维护 | EOL(2023-12) | 活跃维护 | 活跃维护 |
关于 Vue 3 的常见问题
我应该从 Vue 2 迁移到 Vue 3 吗?
是的——Vue 2 于 2023 年 12 月 31 日达到生命周期终点,意味着不再有安全补丁或 bug 修复。迁移难度取决于代码库大小。中小型项目可以使用 @vue/compat 迁移构建直接迁移,该构建启用具有 Vue 2 兼容行为的 Vue 3,并对废弃 API 提供控制台警告。大型项目应规划分阶段迁移:首先更新到 Vue 2.7(它向后移植了 Composition API),然后迁移到 Vue 3。主要破坏性变更:Options API 在 Vue 3 中仍然有效,但 filters 被删除,$on/$off/$once 被删除,一些指令语法发生了变化。
Composition API 和 Options API 有什么区别?
Options API 将组件逻辑组织到预定义的选项桶中:data、methods、computed、watch、mounted 等。这很熟悉,对简单组件有效,但随着相关逻辑散布在多个选项中,可能使大型组件难以理解。Composition API 使用 setup()(或 script setup)作为单一入口点,在那里按功能而非按选项类型组织逻辑。"用户搜索"功能的状态、计算值和 fetch 逻辑都可以放在一起。Composition API 还实现了更好的 TypeScript 推断和用于逻辑复用的组合式函数。两个 API 在 Vue 3 中都完全支持——甚至可以在同一组件中混合使用。
Vue 3 对 TypeScript 支持好吗?
Vue 3 具有出色的 TypeScript 支持。整个 Vue 3 框架用 TypeScript 编写。带有 defineProps<Props>() 的 script setup 语法为 props 提供完整的泛型类型推断。Composition API 提供自然的 TypeScript 集成——ref<string>()、computed<number>() 和类型化的响应式对象都支持完整的 IDE 自动补全。Pinia 完全是 TypeScript 原生的。Volar(官方 VSCode 扩展)提供与 Angular 类型系统相媲美的模板类型检查和自动补全。对于新项目,Vue 3 + TypeScript + Volar 提供一流的类型化开发体验。
Nuxt 3 是什么,什么时候应该使用它?
Nuxt 3 是全栈 Vue 框架,类似于 React 的 Next.js。它构建在 Vue 3、Vite 和 Nitro(通用服务器引擎)之上。Nuxt 3 提供服务器端渲染(SSR)以改善 SEO 和初始加载性能、静态站点生成(SSG)、基于文件的路由、Vue/Pinia/VueUse 组合式函数的自动导入、服务器 API 层(类似于 Next.js API 路由)以及同构数据获取(useFetch、useAsyncData)。当 SEO 重要时(营销网站、电商、博客)、当你需要与前端协同定位的后端 API 时,或当你想要内置电池的 Vue 3 体验时,使用 Nuxt 3。
Vue 3 响应性在底层是如何工作的?
Vue 3 响应性基于 ES6 Proxy。当你创建 reactive() 对象时,Vue 用带有 get/set 拦截器的 Proxy 包装它。当 computed 属性或 watch effect 访问响应式属性时(get 拦截器),Vue 将其追踪为依赖项。当你更新属性时(set 拦截器),Vue 通知所有追踪的依赖项。ref() 类似地工作——它将值包装在带有 .value getter/setter 的对象中。这种基于 Proxy 的方式消除了 Vue 2 的限制:不需要显式 Vue.set() 的深度响应性、数组索引修改有效、添加到对象的新属性是响应式的。Computed 值是记忆化惰性的:只有在依赖项变化且 computed 被访问时才计算。
Vue 3 中什么取代了 Vuex?
Pinia 是 Vue 3 中 Vuex 的官方替代品,由 Vue 核心团队推荐。它由 Eduardo San Martin Morote(Vue 核心团队成员)创建,围绕 Vue 3 Composition API 设计。与 Vuex 的主要区别:没有 mutations(只有 state、getters 和 actions)、无需复杂类型解决方案的完整 TypeScript 支持、没有嵌套模块(只是可以相互导入的独立 store)、更轻量的 API,以及原生 Vue DevTools 集成。Vuex 4 仍然适用于 Vue 3,但处于维护模式——所有新项目应使用 Pinia。
我可以在没有构建步骤的情况下使用 Vue 3 吗?
可以——Vue 3 可以作为纯 CDN 脚本使用,用于现有 HTML 页面的渐进增强,不需要构建工具。全局构建(vue.global.js)在 Vue 全局上暴露所有 API。ESM 浏览器构建(vue.esm-browser.js)可以直接在原生 ES 模块中使用。对于使用单文件组件(.vue 文件)的复杂应用,你需要构建步骤——Vite 是推荐工具,提供近乎即时的开发服务器启动、亚秒级热模块替换和优化的生产构建。Vue Playground(play.vuejs.org)让你无需任何设置即可在线试用 Vue 3。
v-if 和 v-show 有什么区别?
当条件变化时,v-if 完全渲染或销毁元素及其子元素。当为 false 时,元素不存在于 DOM 中——它运行生命周期钩子(onMounted/onUnmounted),处理子组件的创建/销毁,并具有更高的切换成本。v-show 始终渲染元素但切换 CSS display:none。它具有更低的切换成本但更高的初始渲染成本(始终渲染)。当条件很少变化或想要懒渲染重量级组件时使用 v-if。对于频繁切换的内容(标签页、手风琴),在想要避免重新渲染成本时使用 v-show。