Vue.js 是目前最流行的前端框架之一,以其渐进式、易上手和高性能著称。本篇文章以 Vue 3 组合式 API 为主线,带你从入门到组件化开发实战。
一、Vue 3 组合式 API(Composition API)
为什么选择组合式 API?
Vue 2 的选项式 API(Options API)按选项组织代码(data、methods、computed),当组件复杂度上升时,同一逻辑被分散在不同选项中,难以维护。组合式 API 允许按逻辑关注点组织代码。
| 维度 | 选项式 API(Vue 2) | 组合式 API(Vue 3) |
|---|---|---|
| 代码组织 | 按选项类型分离 | 按逻辑关注点聚合 |
| 逻辑复用 | Mixins(命名冲突、来源不明) | Composables(清晰、可组合) |
| TypeScript 支持 | 较差 | 优秀 |
| Tree Shaking | 不支持 | 支持 |
| 学习曲线 | 低 | 中等 |
setup / ref / reactive
<script setup lang="ts">
import { ref, reactive, computed, watch } from 'vue';
// ref:包装基本类型和对象(推荐)
const count = ref(0);
const message = ref('Hello Vue 3');
const user = ref({ name: 'Alice', age: 25 });
// reactive:包装对象(有局限性)
const state = reactive({
count: 0,
items: []
});
// computed:计算属性(自动追踪依赖,缓存结果)
const doubleCount = computed(() => count.value * 2);
// 可写计算属性
const fullName = computed({
get: () => `${firstName.value} ${lastName.value}`,
set: (val) => {
[firstName.value, lastName.value] = val.split(' ');
}
});
// watch:监听数据变化
watch(count, (newVal, oldVal) => {
console.log(`count changed: ${oldVal} → ${newVal}`);
});
// 监听多个源
watch([count, message], ([newCount, newMsg]) => {
console.log(`count: ${newCount}, msg: ${newMsg}`);
});
// watchEffect:自动追踪依赖
watchEffect(() => {
console.log(`count is: ${count.value}`);
// 自动追踪 count.value,无需显式声明
});
// 方法
function increment() {
count.value++; // ref 在 script 中需要 .value
}
</script>
<template>
<!-- template 中自动解包,不需要 .value -->
<div>
<p>Count: {{ count }}</p>
<p>Double: {{ doubleCount }}</p>
<button @click="increment">+1</button>
</div>
</template>ref vs reactive 选择建议:
- ref 优先:可以包装任意类型,解构不丢失响应性,
.value明确标识响应式数据。- reactive:适合表单对象等不需要解构的场景。但解构会丢失响应性(需要
toRefs),不能被重新赋值。
二、模板语法与指令
<template>
<!-- 文本插值 -->
<p>{{ message }}</p>
<!-- v-bind:动态绑定属性(简写 :) -->
<img :src="imageUrl" :alt="imageAlt">
<div :class="{ active: isActive, 'text-danger': hasError }"></div>
<div :style="{ color: textColor, fontSize: size + 'px' }"></div>
<!-- v-on:事件监听(简写 @) -->
<button @click="handleClick">点击</button>
<input @keyup.enter="submit" @input="handleInput">
<div @click.stop="stopPropagation">阻止冒泡</div>
<form @submit.prevent="onSubmit">阻止默认</form>
<!-- v-model:双向绑定 -->
<input v-model="username" placeholder="用户名">
<!-- 等价于 -->
<input :value="username" @input="username = $event.target.value">
<!-- v-model 修饰符 -->
<input v-model.lazy="msg"> <!-- change 事件时更新 -->
<input v-model.number="age"> <!-- 自动转为数字 -->
<input v-model.trim="text"> <!-- 去除首尾空格 -->
<!-- v-if / v-else-if / v-else:条件渲染 -->
<div v-if="type === 'A'">类型 A</div>
<div v-else-if="type === 'B'">类型 B</div>
<div v-else>其他类型</div>
<!-- v-show:条件显示(始终渲染,切换 display) -->
<div v-show="isVisible">可见/隐藏切换</div>
<!-- v-for:列表渲染 -->
<ul>
<li v-for="(item, index) in items" :key="item.id">
{{ index }} - {{ item.name }}
</li>
</ul>
<!-- v-for 遍历对象 -->
<li v-for="(value, key, index) in user" :key="key">
{{ key }}: {{ value }}
</li>
<!-- v-once:只渲染一次 -->
<span v-once>这个值不会更新:{{ expensiveComputation }}</span>
<!-- v-html:渲染 HTML(⚠️ 注意 XSS 风险) -->
<div v-html="trustedHtml"></div>
</template>| 指令 | 用途 | 简写 |
|---|---|---|
v-bind |
动态绑定属性/类/样式 | : |
v-on |
事件监听 | @ |
v-model |
双向数据绑定 | — |
v-if / v-else-if / v-else |
条件渲染(销毁/重建) | — |
v-show |
条件显示(display 切换) | — |
v-for |
列表渲染 | — |
v-once |
一次性渲染 | — |
v-html |
渲染 HTML 字符串 | — |
v-if vs v-show:频繁切换用
v-show(CSS 开销),条件很少改变用v-if(避免初始渲染开销)。
三、组件通信
Props 与 Emits
<!-- ===== 父组件 ===== -->
<template>
<ChildComponent
:title="postTitle"
:likes="42"
@update="handleUpdate"
/>
</template>
<!-- ===== 子组件 ChildComponent.vue ===== -->
<script setup lang="ts">
// Props 定义
interface Props {
title: string;
likes?: number; // 可选
tags?: string[]; // 带默认值
}
const props = withDefaults(defineProps<Props>(), {
likes: 0,
tags: () => []
});
// Emits 定义
const emit = defineEmits<{
update: [value: string]; // 单参数
delete: [id: number]; // 单参数
'update:modelValue': [value: any]; // v-model 事件
}>();
function handleClick() {
emit('update', 'new value');
}
</script>
<template>
<div>
<h3>{{ props.title }}</h3>
<p>Likes: {{ props.likes }}</p>
<button @click="handleClick">更新</button>
</div>
</template>v-model 组件通信
<!-- ===== 父组件 ===== -->
<template>
<CustomInput v-model="searchText" />
<!-- 等价于 -->
<CustomInput
:model-value="searchText"
@update:model-value="searchText = $event"
/>
<!-- 多个 v-model -->
<UserForm
v-model:name="userName"
v-model:email="userEmail"
/>
</template>
<!-- ===== CustomInput.vue ===== -->
<script setup lang="ts">
const props = defineProps<{ modelValue: string }>();
const emit = defineEmits<{ 'update:modelValue': [value: string] }>();
function onInput(e: Event) {
emit('update:modelValue', (e.target as HTMLInputElement).value);
}
</script>
<template>
<input :value="modelValue" @input="onInput" />
</template>Provide / Inject(跨层级通信)
<!-- ===== 祖先组件 ===== -->
<script setup>
import { provide, ref, readonly } from 'vue';
const theme = ref('light');
const updateTheme = (t) => { theme.value = t; };
provide('theme', readonly(theme)); // 只读,防止子组件直接修改
provide('updateTheme', updateTheme);
</script>
<!-- ===== 深层子组件 ===== -->
<script setup>
import { inject } from 'vue';
const theme = inject('theme', 'light'); // 带默认值
const updateTheme = inject('updateTheme');
</script>
<template>
<div :class="theme">
<button @click="updateTheme('dark')">切换暗色</button>
</div>
</template>| 通信方式 | 适用场景 | 优点 | 缺点 |
|---|---|---|---|
| Props ↓ / Emits ↑ | 父子直连 | 清晰、类型安全 | 多层级繁琐 |
| v-model | 表单组件 | 简洁优雅 | 适合单一值 |
| Provide/Inject | 跨层级 | 无需逐层传递 | 数据来源不直观 |
| Pinia / Vuex | 全局状态 | 任意组件访问 | 增加复杂度 |
| Event Bus (mitt) | 任意组件 | 灵活 | 不推荐,难以追踪 |
四、生命周期钩子
<script setup>
import {
onBeforeMount, onMounted,
onBeforeUpdate, onUpdated,
onBeforeUnmount, onUnmounted,
onActivated, onDeactivated, // <KeepAlive> 专用
onErrorCaptured,
} from 'vue';
// 初始化(setup 本身 = beforeCreate + created)
console.log('组件初始化');
onBeforeMount(() => {
// 渲染前,DOM 尚未创建
});
onMounted(() => {
// DOM 已挂载,可以操作 DOM、发起请求
fetchData();
});
onBeforeUpdate(() => {
// 数据变化,DOM 更新前
});
onUpdated(() => {
// DOM 已更新
});
onBeforeUnmount(() => {
// 组件卸载前,清理定时器、事件监听
clearInterval(timer);
});
onUnmounted(() => {
// 组件已卸载
});
</script>| 选项式 API | 组合式 API | 说明 |
|---|---|---|
beforeCreate |
setup() |
setup 本身 |
created |
setup() |
setup 本身 |
beforeMount |
onBeforeMount |
模板编译完成,DOM 未挂载 |
mounted |
onMounted |
DOM 已挂载,可访问 $el |
beforeUpdate |
onBeforeUpdate |
响应式数据变化,DOM 更新前 |
updated |
onUpdated |
DOM 更新后 |
beforeUnmount |
onBeforeUnmount |
卸载前,适合清理 |
unmounted |
onUnmounted |
卸载完成 |
五、路由(Vue Router)
// router/index.ts
import { createRouter, createWebHistory } from 'vue-router';
const routes = [
{
path: '/',
name: 'Home',
component: () => import('@/views/Home.vue'), // 懒加载
},
{
path: '/user/:id',
name: 'UserDetail',
component: () => import('@/views/UserDetail.vue'),
props: true, // 将 params 作为 props 传递
meta: { requiresAuth: true },
},
{
path: '/admin',
component: () => import('@/layouts/AdminLayout.vue'),
children: [
{
path: '', // /admin
component: () => import('@/views/admin/Dashboard.vue'),
},
{
path: 'users', // /admin/users
component: () => import('@/views/admin/Users.vue'),
},
],
},
{
path: '/:pathMatch(.*)*', // 404 通配
name: 'NotFound',
component: () => import('@/views/NotFound.vue'),
},
];
const router = createRouter({
history: createWebHistory(),
routes,
scrollBehavior(to, from, savedPosition) {
if (savedPosition) return savedPosition;
return { top: 0 };
},
});
// 路由守卫
router.beforeEach((to, from) => {
if (to.meta.requiresAuth && !isAuthenticated()) {
return { name: 'Login', query: { redirect: to.fullPath } };
}
});
export default router;<!-- 组件中使用路由 -->
<script setup>
import { useRouter, useRoute } from 'vue-router';
const router = useRouter();
const route = useRoute();
// 编程式导航
function goToUser(id: number) {
router.push({ name: 'UserDetail', params: { id } });
}
// 获取参数
const userId = computed(() => route.params.id);
const searchQuery = computed(() => route.query.q);
// 路由信息
console.log(route.path, route.name, route.meta);
</script>
<template>
<nav>
<!-- 声明式导航 -->
<router-link to="/">首页</router-link>
<router-link :to="{ name: 'UserDetail', params: { id: 123 } }">
用户 123
</router-link>
</nav>
<!-- 路由出口 -->
<router-view v-slot="{ Component, route }">
<transition name="fade" mode="out-in">
<component :is="Component" :key="route.path" />
</transition>
</router-view>
</template>六、状态管理(Pinia)
Pinia 是 Vue 官方推荐的状态管理库,比 Vuex 更简洁、更符合组合式 API 风格。
// stores/user.ts
import { defineStore } from 'pinia';
import { ref, computed } from 'vue';
export const useUserStore = defineStore('user', () => {
// State
const user = ref<User | null>(null);
const token = ref<string>('');
// Getters(computed)
const isLoggedIn = computed(() => !!token.value);
const userName = computed(() => user.value?.name ?? '游客');
// Actions
async function login(credentials: { email: string; password: string }) {
const response = await fetch('/api/login', {
method: 'POST',
body: JSON.stringify(credentials),
});
const data = await response.json();
user.value = data.user;
token.value = data.token;
localStorage.setItem('token', data.token);
}
function logout() {
user.value = null;
token.value = '';
localStorage.removeItem('token');
}
// 也可以使用选项式风格
// ...
return { user, token, isLoggedIn, userName, login, logout };
});<!-- 组件中使用 Pinia -->
<script setup>
import { useUserStore } from '@/stores/user';
import { storeToRefs } from 'pinia';
const userStore = useUserStore();
// storeToRefs:保持响应性
const { user, isLoggedIn, userName } = storeToRefs(userStore);
// actions 直接解构即可(不需要响应性)
const { login, logout } = userStore;
</script>
<template>
<div v-if="isLoggedIn">
<p>欢迎,{{ userName }}</p>
<button @click="logout">退出</button>
</div>
<div v-else>
<button @click="login({ email: 'a@b.com', password: '123' })">
登录
</button>
</div>
</template>Pinia vs Vuex 对比
| 特性 | Pinia | Vuex 4 |
|---|---|---|
| 体积 | ~1.5 KB | ~10 KB |
| 类型支持 | 完美 TypeScript | 较弱 |
| API 设计 | 组合式 + 选项式 | 仅选项式 |
| 模块化 | 天然支持(多个 Store) | 需要 modules |
| 去掉了 Mutations | ✅ 直接修改 state | 需要 commit |
| DevTools | ✅ | ✅ |
| 官方推荐 | ✅ Vue 3 首选 | 遗留项目 |
七、与 React 的简单对比
| 维度 | Vue 3 | React 18 |
|---|---|---|
| 核心范式 | 响应式(自动追踪) | 不可变(手动 setState) |
| 模板 | SFC 模板(HTML-like) | JSX(JavaScript + HTML) |
| 响应式系统 | Proxy 自动追踪 | 手动声明依赖(useMemo/useCallback) |
| 组件更新 | 自动精准更新 | 自上而下重新渲染(需优化) |
| 状态管理 | Pinia(内置响应式) | Redux / Zustand / Jotai |
| 路由 | Vue Router(官方) | React Router |
| 学习曲线 | 平缓(模板接近 HTML) | 较陡(JSX + 更多概念) |
| 生态系统 | 官方集中维护 | 社区分散 |
| 适用场景 | 中小型到中大型 | 任意规模,生态更丰富 |
<!-- Vue:模板语法 -->
<template>
<div>
<p>{{ count }}</p>
<button @click="increment">+1</button>
</div>
</template>// React:JSX
function Counter() {
const [count, setCount] = useState(0);
return (
<div>
<p>{count}</p>
<button onClick={() => setCount(c => c + 1)}>+1</button>
</div>
);
}八、实用 Composables 示例
// composables/useFetch.ts
import { ref, watchEffect, toValue, type MaybeRefOrGetter } from 'vue';
export function useFetch<T>(url: MaybeRefOrGetter<string>) {
const data = ref<T | null>(null);
const error = ref<Error | null>(null);
const loading = ref(false);
async function execute() {
loading.value = true;
error.value = null;
try {
const response = await fetch(toValue(url));
data.value = await response.json();
} catch (e) {
error.value = e as Error;
} finally {
loading.value = false;
}
}
// url 变化时自动重新请求
watchEffect(() => {
toValue(url); // 追踪依赖
execute();
});
return { data, error, loading, refetch: execute };
}
// 使用
const { data: users, loading } = useFetch<User[]>('/api/users');// composables/useDebounce.ts
import { ref, watch, type Ref } from 'vue';
export function useDebounce<T>(source: Ref<T>, delay = 300) {
const debounced = ref(source.value) as Ref<T>;
let timer: ReturnType<typeof setTimeout>;
watch(source, (val) => {
clearTimeout(timer);
timer = setTimeout(() => {
debounced.value = val;
}, delay);
});
return debounced;
}九、最佳实践速查
| 实践 | 说明 |
|---|---|
✅ <script setup> + TypeScript |
官方推荐,更简洁 |
| ✅ Composables 提取复用逻辑 | 替代 Mixins |
✅ ref 优先于 reactive |
更灵活,解构安全 |
| ✅ 路由懒加载 | () => import(...) |
✅ defineProps + defineEmits 类型声明 |
编译时类型检查 |
✅ shallowRef / shallowReactive 优化大列表 |
减少深层响应式开销 |
| ❌ 不要直接修改 props | 单向数据流 |
❌ 不要在 computed 中做副作用 |
用 watch 代替 |
❌ 避免 v-if 和 v-for 同时使用 |
v-if 优先级更高,性能差 |
结语
Vue 3 的组合式 API 不仅带来了更好的逻辑组织方式,也在 TypeScript 支持上迈出了一大步。从入门到组件化开发,关键在于:
- 理解响应式:ref / reactive / computed / watch
- 掌握组件通信:props ↓ / emits ↑ / provide-inject
- 善用生态:Vue Router + Pinia + Composables
配合官方文档和实际项目练习,你会很快感受到 Vue 开发的乐趣。前端三件套(HTML / CSS / JS)+ 一个主流框架(Vue / React),就是现代 Web 开发的完整技能树!