前言
💡 痛点: Composition API 和 Options API 怎么选?Composables 如何设计成可复用?Vue 3 的响应式系统原理是什么?TypeScript 和 Vue 3 如何完美结合?
🎯 解决方案: 从响应式原理→组合式函数→依赖注入→Pinia→TypeScript→性能优化→测试,系统掌握 Vue 3 Composition API。
#mermaid-svg-cLBRG56zb72K9TV3{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-cLBRG56zb72K9TV3 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-cLBRG56zb72K9TV3 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-cLBRG56zb72K9TV3 .error-icon{fill:#552222;}#mermaid-svg-cLBRG56zb72K9TV3 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-cLBRG56zb72K9TV3 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-cLBRG56zb72K9TV3 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-cLBRG56zb72K9TV3 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-cLBRG56zb72K9TV3 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-cLBRG56zb72K9TV3 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-cLBRG56zb72K9TV3 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-cLBRG56zb72K9TV3 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-cLBRG56zb72K9TV3 .marker.cross{stroke:#333333;}#mermaid-svg-cLBRG56zb72K9TV3 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-cLBRG56zb72K9TV3 p{margin:0;}#mermaid-svg-cLBRG56zb72K9TV3 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-cLBRG56zb72K9TV3 .cluster-label text{fill:#333;}#mermaid-svg-cLBRG56zb72K9TV3 .cluster-label span{color:#333;}#mermaid-svg-cLBRG56zb72K9TV3 .cluster-label span p{background-color:transparent;}#mermaid-svg-cLBRG56zb72K9TV3 .label text,#mermaid-svg-cLBRG56zb72K9TV3 span{fill:#333;color:#333;}#mermaid-svg-cLBRG56zb72K9TV3 .node rect,#mermaid-svg-cLBRG56zb72K9TV3 .node circle,#mermaid-svg-cLBRG56zb72K9TV3 .node ellipse,#mermaid-svg-cLBRG56zb72K9TV3 .node polygon,#mermaid-svg-cLBRG56zb72K9TV3 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-cLBRG56zb72K9TV3 .rough-node .label text,#mermaid-svg-cLBRG56zb72K9TV3 .node .label text,#mermaid-svg-cLBRG56zb72K9TV3 .image-shape .label,#mermaid-svg-cLBRG56zb72K9TV3 .icon-shape .label{text-anchor:middle;}#mermaid-svg-cLBRG56zb72K9TV3 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-cLBRG56zb72K9TV3 .rough-node .label,#mermaid-svg-cLBRG56zb72K9TV3 .node .label,#mermaid-svg-cLBRG56zb72K9TV3 .image-shape .label,#mermaid-svg-cLBRG56zb72K9TV3 .icon-shape .label{text-align:center;}#mermaid-svg-cLBRG56zb72K9TV3 .node.clickable{cursor:pointer;}#mermaid-svg-cLBRG56zb72K9TV3 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-cLBRG56zb72K9TV3 .arrowheadPath{fill:#333333;}#mermaid-svg-cLBRG56zb72K9TV3 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-cLBRG56zb72K9TV3 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-cLBRG56zb72K9TV3 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-cLBRG56zb72K9TV3 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-cLBRG56zb72K9TV3 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-cLBRG56zb72K9TV3 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-cLBRG56zb72K9TV3 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-cLBRG56zb72K9TV3 .cluster text{fill:#333;}#mermaid-svg-cLBRG56zb72K9TV3 .cluster span{color:#333;}#mermaid-svg-cLBRG56zb72K9TV3 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-cLBRG56zb72K9TV3 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-cLBRG56zb72K9TV3 rect.text{fill:none;stroke-width:0;}#mermaid-svg-cLBRG56zb72K9TV3 .icon-shape,#mermaid-svg-cLBRG56zb72K9TV3 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-cLBRG56zb72K9TV3 .icon-shape p,#mermaid-svg-cLBRG56zb72K9TV3 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-cLBRG56zb72K9TV3 .icon-shape .label rect,#mermaid-svg-cLBRG56zb72K9TV3 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-cLBRG56zb72K9TV3 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-cLBRG56zb72K9TV3 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-cLBRG56zb72K9TV3 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 状态管理
依赖注入
组合式函数
Vue 3 响应式系统
业务库
工具库
生命周期
onMounted
onUnmounted
onWatcher
ref
基础响应式
reactive
对象响应式
computed
计算属性
watch/watchEffect
监听器
useCounter
状态逻辑
useFetch
数据获取
useStorage
持久化
useAuth
认证
usePagination
分页
provide
提供
inject
注入
Pinia
全局状态
Plugin
插件
Vue 3 Composition API 核心优势:
| 优势 | 说明 | 对比 Options API |
|---|---|---|
| 逻辑复用 | Composable 函数提取逻辑 | Mixins 有命名冲突/来源不明 |
| 更好的类型推导 | TypeScript 完美支持 | Options API 需复杂泛型 |
| 更灵活的代码组织 | 按逻辑而非选项分组 | 同一逻辑分散在 data/methods/computed |
| 更小的生产包 | Tree-shaking 支持 | 全部打包 |
| 更好的性能 | 更好的虚拟 DOM diff | - |
一、响应式系统原理
1.1 ref 与 reactive
typescript
// ===== ref:基础类型响应式 =====
import { ref, computed, watch } from 'vue';
// ref 包装基础类型
const count = ref(0);
// .value 访问(模板中自动解包)
console.log(count.value); // 0
count.value++;
console.log(count.value); // 1
// ref 对象结构
const doubleCount = computed(() => count.value * 2);
// ref 也接受复杂对象
const timerRef = ref<number | null>(null);
timerRef.value = setInterval(() => { /* ... */ }, 1000);
// ===== reactive:对象响应式 =====
import { reactive, readonly, toRefs } from 'vue';
// reactive 只能用于对象/数组
const state = reactive({
user: { name: '张三', age: 28 },
posts: [],
loading: false,
});
// 自动解包(模板中)
state.user.name = '李四';
state.loading = true;
// readonly:防止外部修改
const stateCopy = readonly(state);
// toRefs:将 reactive 对象转为一堆 ref
const { user, loading } = toRefs(state);
// → user 是 ref<{name, age}>
// → 修改 user.value 会影响原 state
// ===== 响应式陷阱 =====
// ❌ 错误:解构丢失响应式
const { name, age } = state; // 失去响应式!
// ✅ 正确:使用 toRefs
const stateRefs = toRefs(state);
const { name, age } = stateRefs;
// ❌ 错误:reactive 替换整个对象
state = reactive({ new: 'data' }); // 丢失响应式
// ✅ 正确:逐个属性修改
Object.assign(state, { new: 'data' });
// ❌ 错误:数组索引直接替换
state.posts[0] = newPost; // 不触发更新
// ✅ 正确:使用 splice 或替换整个数组
state.posts.splice(0, 1, newPost);
// 或
state.posts = [...state.posts.slice(0, i), newPost, ...state.posts.slice(i + 1)];
1.2 响应式原理深度解析
typescript
// ===== Vue 3 响应式原理 =====
// Vue 3 使用 Proxy 实现响应式
// 相比 Vue 2 的 Object.defineProperty:
// ✅ 支持数组下标响应式
// ✅ 支持新增属性响应式(不用 Vue.set)
// ✅ 支持 Map/Set/WeakMap/WeakSet
import { reactive, effect, computed } from 'vue';
// 手写简化版响应式系统
function myReactive<T extends object>(target: T): T {
return new Proxy(target, {
get(target, key, receiver) {
const result = Reflect.get(target, key, receiver);
// 依赖收集
if (activeEffect) {
// 建立 key → effect 的映射
depsMap.get(key)?.add(activeEffect)
?? depsMap.set(key, new Set([activeEffect])).get(key);
}
return typeof result === 'object' && result !== null
? myReactive(result) // 深度代理
: result;
},
set(target, key, value, receiver) {
const oldValue = Reflect.get(target, key, receiver);
const result = Reflect.set(target, key, value, receiver);
if (oldValue !== value) {
// 触发更新
trigger(target, key);
}
return result;
},
deleteProperty(target, key) {
const hadKey = Object.prototype.hasOwnProperty.call(target, key);
const result = Reflect.deleteProperty(target, key);
if (hadKey && result) {
trigger(target, key);
}
return result;
},
});
}
// ===== watch vs watchEffect =====
import { watch, watchEffect, ref } from 'vue';
const userId = ref(1);
const userData = ref(null);
// watchEffect:立即执行,依赖自动追踪
// 当 userData.value 变化时自动重新执行
watchEffect(async () => {
userData.value = await fetchUser(userId.value);
console.log('fetched for:', userId.value);
});
// watch:惰性执行,精确控制
// 当 userId.value 变化时才执行
watch(userId, async (newId, oldId) => {
userData.value = await fetchUser(newId);
console.log(`user changed: ${oldId} → ${newId}`);
});
// 深度监听对象
watch(userData, (newData) => {
console.log('user changed:', newData);
}, { deep: true });
// 监听多个源
watch([userId, userData], ([newId, newData], [oldId, oldData]) => {
console.log('changed:', newId, newData);
});
// ===== computed 原理 =====
function myComputed<T>(getter: () => T) {
let value: T;
let dirty = true; // 脏标记
const effectFn = () => {
dirty = true; // 依赖变化时标记为脏
};
// 追踪依赖
watchEffect(effectFn);
return {
get value() {
if (dirty) {
value = getter();
dirty = false;
}
return value;
},
};
}
二、组合式函数(Composables)
2.1 基础 Composable 设计
typescript
// ===== useCounter 计数器 =====
// composables/useCounter.ts
import { ref, computed } from 'vue';
export function useCounter(options: {
min?: number;
max?: number;
} = {}) {
const count = ref(0);
const min = options.min ?? -Infinity;
const max = options.max ?? Infinity;
const canIncrement = computed(() => count.value < max);
const canDecrement = computed(() => count.value > min);
function increment(delta = 1) {
if (canIncrement.value) {
count.value = Math.min(count.value + delta, max);
}
}
function decrement(delta = 1) {
if (canDecrement.value) {
count.value = Math.max(count.value - delta, min);
}
}
function reset() {
count.value = 0;
}
// 返回响应式状态和操作方法
return {
count, // Ref<number>
canIncrement,
canDecrement,
increment,
decrement,
reset,
};
}
// ===== useLocalStorage 持久化 =====
// composables/useLocalStorage.ts
import { ref, watch, onMounted } from 'vue';
export function useLocalStorage<T>(
key: string,
defaultValue: T
) {
// 初始化:优先从 localStorage 读取
const stored = localStorage.getItem(key);
const data = ref<T>(stored ? JSON.parse(stored) : defaultValue);
// 监听变化,自动持久化
watch(
data,
(newValue) => {
if (newValue === null || newValue === undefined) {
localStorage.removeItem(key);
} else {
localStorage.setItem(key, JSON.stringify(newValue));
}
},
{ deep: true }
);
// SSR 兼容
onMounted(() => {
const stored = localStorage.getItem(key);
if (stored) {
data.value = JSON.parse(stored);
}
});
return data;
}
// 使用
const theme = useLocalStorage('theme', 'light');
// theme.value 自动持久化到 localStorage
// ===== useDebounceFn 防抖 =====
// composables/useDebounceFn.ts
import { ref, onUnmounted } from 'vue';
export function useDebounceFn<T extends (...args: any[]) => any>(
fn: T,
delay = 300
) {
let timer: ReturnType<typeof setTimeout> | null = null;
const pending = ref(false);
function debouncedFn(...args: Parameters<T>) {
pending.value = true;
if (timer) clearTimeout(timer);
timer = setTimeout(() => {
fn(...args);
pending.value = false;
timer = null;
}, delay);
}
// 组件卸载时清理
onUnmounted(() => {
if (timer) clearTimeout(timer);
});
return {
fn: debouncedFn,
pending,
cancel: () => {
if (timer) {
clearTimeout(timer);
timer = null;
pending.value = false;
}
},
};
}
// 使用
const { fn: searchUsers, pending } = useDebounceFn(
async (query: string) => {
users.value = await api.search(query);
},
300
);
2.2 业务级 Composable
typescript
// ===== usePagination 分页 =====
// composables/usePagination.ts
import { ref, computed, watch } from 'vue';
export interface PaginationOptions<T> {
/** 数据源 */
data: Ref<T[]>;
/** 每页条数 */
pageSize?: number;
/** 初始页码 */
initialPage?: number;
}
export interface PaginationReturn<T> {
/** 当前页数据 */
currentItems: ComputedRef<T[]>;
/** 总条数 */
total: ComputedRef<number>;
/** 总页数 */
totalPages: ComputedRef<number>;
/** 当前页码 */
currentPage: Ref<number>;
/** 每页条数 */
pageSize: Ref<number>;
/** 是否第一页 */
isFirst: ComputedRef<boolean>;
/** 是否最后一页 */
isLast: ComputedRef<boolean>;
/** 页码列表 */
pageNumbers: ComputedRef<number[]>;
/** 操作方法 */
nextPage: () => void;
prevPage: () => void;
goToPage: (page: number) => void;
reset: () => void;
}
export function usePagination<T>(
options: PaginationOptions<T>
): PaginationReturn<T> {
const { data, pageSize: initPageSize = 10, initialPage = 1 } = options;
const currentPage = ref(initialPage);
const pageSize = ref(initPageSize);
const total = computed(() => data.value.length);
const totalPages = computed(() => Math.ceil(total.value / pageSize.value));
const currentItems = computed(() => {
const start = (currentPage.value - 1) * pageSize.value;
return data.value.slice(start, start + pageSize.value);
});
const isFirst = computed(() => currentPage.value === 1);
const isLast = computed(() => currentPage.value === totalPages.value);
const pageNumbers = computed(() => {
const pages: number[] = [];
const max = 7; // 显示的页码数量
let start = Math.max(1, currentPage.value - Math.floor(max / 2));
let end = Math.min(totalPages.value, start + max - 1);
if (end - start + 1 < max) {
start = Math.max(1, end - max + 1);
}
for (let i = start; i <= end; i++) {
pages.push(i);
}
return pages;
});
function nextPage() {
if (!isLast.value) currentPage.value++;
}
function prevPage() {
if (!isFirst.value) currentPage.value--;
}
function goToPage(page: number) {
currentPage.value = Math.max(1, Math.min(page, totalPages.value));
}
function reset() {
currentPage.value = 1;
}
// 数据源变化时重置
watch(data, reset);
return {
currentItems,
total,
totalPages,
currentPage,
pageSize,
isFirst,
isLast,
pageNumbers,
nextPage,
prevPage,
goToPage,
reset,
};
}
// 使用
const allPosts = ref<Post[]>([]);
const {
currentItems: displayedPosts,
total,
currentPage,
pageNumbers,
nextPage,
prevPage,
goToPage,
} = usePagination({ data: allPosts, pageSize: 20 });
2.3 useFetch 数据获取
typescript
// ===== useFetch 封装 =====
// composables/useFetch.ts
import { ref, watchEffect, isRef, type Ref, type ComputedRef } from 'vue';
export interface UseFetchOptions {
/** 立即请求 */
immediate?: boolean;
/** 刷新依赖 */
refreshDeps?: Array<Ref | ComputedRef>;
/** 请求配置 */
credentials?: RequestCredentials;
/** 超时时间(ms)*/
timeout?: number;
}
export interface UseFetchReturn<T> {
data: Ref<T | null>;
error: Ref<Error | null>;
isLoading: Ref<boolean>;
isFetching: Ref<boolean>;
isSuccess: Ref<boolean>;
execute: (url?: string) => Promise<void>;
refresh: () => Promise<void>;
abort: () => void;
}
export function useFetch<T>(
url: string | Ref<string>,
options: UseFetchOptions = {}
): UseFetchReturn<T> {
const {
immediate = true,
refreshDeps = [],
credentials = 'same-origin',
timeout = 30000,
} = options;
const data = ref<T | null>(null) as Ref<T | null>;
const error = ref<Error | null>(null);
const isLoading = ref(false);
const isFetching = ref(false);
const isSuccess = ref(false);
let controller: AbortController | null = null;
let timeoutId: ReturnType<typeof setTimeout> | null = null;
async function execute(urlToFetch?: string) {
const currentUrl = urlToFetch ?? (isRef(url) ? url.value : url);
if (!currentUrl) return;
// 取消之前的请求
abort();
controller = new AbortController();
isLoading.value = true;
isFetching.value = true;
error.value = null;
isSuccess.value = false;
try {
const response = await Promise.race([
fetch(currentUrl, {
method: 'GET',
headers: { 'Content-Type': 'application/json' },
credentials,
signal: controller.signal,
}),
new Promise<never>((_, reject) => {
timeoutId = setTimeout(() => reject(new Error('请求超时')), timeout);
}),
]);
clearTimeout(timeoutId!);
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
data.value = await response.json();
isSuccess.value = true;
} catch (e: any) {
if (e.name !== 'AbortError') {
error.value = e;
data.value = null;
}
} finally {
isLoading.value = false;
isFetching.value = false;
}
}
function abort() {
controller?.abort();
controller = null;
if (timeoutId) {
clearTimeout(timeoutId);
timeoutId = null;
}
}
async function refresh() {
await execute();
}
if (immediate) {
if (refreshDeps.length > 0) {
// 依赖变化时自动刷新
watchEffect(() => {
// 收集依赖
refreshDeps.forEach(dep => {
if (isRef(dep)) dep.value;
});
execute();
});
} else {
execute();
}
}
return {
data,
error,
isLoading,
isFetching,
isSuccess,
execute,
refresh,
abort,
};
}
// ===== POST 请求封装 =====
export function usePost<T, R>(
url: string | Ref<string>,
body: Ref<T> | (() => T)
) {
const result = ref<R | null>(null);
const error = ref<Error | null>(null);
const isLoading = ref(false);
async function execute() {
isLoading.value = true;
error.value = null;
try {
const payload = typeof body === 'function' ? body() : body.value;
const response = await fetch(isRef(url) ? url.value : url, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
});
if (!response.ok) throw new Error(`HTTP ${response.status}`);
result.value = await response.json();
} catch (e: any) {
error.value = e;
} finally {
isLoading.value = false;
}
}
return { result, error, isLoading, execute };
}
// ===== 使用示例 =====
// 基础 GET
const { data: user, isLoading, refresh } = useFetch<User>('/api/user/1');
// 带依赖刷新(userId 变化时自动重新请求)
const userId = ref(1);
const { data: userProfile } = useFetch<User>(
computed(() => `/api/user/${userId.value}/profile`),
{ refreshDeps: [userId] }
);
// POST 请求
const postData = ref({ name: '', email: '' });
const { result, isLoading: posting, execute: submit } = usePost('/api/users', postData);
三、依赖注入
3.1 provide/inject
typescript
// ===== 基础依赖注入 =====
// Parent.vue(提供方)
import { provide, ref } from 'vue';
export default {
setup() {
const theme = ref('light');
const currentUser = ref({ id: 1, name: '张三' });
// provide(key, value)
provide('theme', theme); // 响应式
provide('theme-mode', 'dark'); // 静态值
provide('currentUser', currentUser); // 响应式对象
provide('api', new ApiService()); // 类实例
// provide 也支持 symbol 作为 key
const USER_KEY = Symbol('user');
provide(USER_KEY, currentUser);
// 更新响应式值,子组件自动同步
function setTheme(newTheme: string) {
theme.value = newTheme;
}
return { setTheme };
},
};
// Child.vue(注入方)
import { inject, computed } from 'vue';
export default {
setup() {
// 注入
const theme = inject<Ref<string>>('theme'); // 响应式
const themeMode = inject<string>('theme-mode'); // 静态值
const user = inject('currentUser') as Ref<User>; // 断言类型
// 注入带默认值
const config = inject('appConfig', { apiUrl: '/api' });
// 注入 symbol key
const userBySymbol = inject(Symbol('user'));
// 计算属性使用注入值
const isDark = computed(() => theme?.value === 'dark');
// inject 也支持 reactive 解构
const { theme: injectedTheme } = inject<{
theme: Ref<string>;
user: Ref<User>;
}>('app-context', {} as any);
return { theme, themeMode, user, isDark };
},
};
3.2 依赖注入进阶模式
typescript
// ===== Context Provider 封装 =====
// composables/createContext.ts
import { InjectionKey, provide, inject } from 'vue';
export interface AuthContext {
user: Ref<User | null>;
isAuthenticated: ComputedRef<boolean>;
login: (credentials: LoginCredentials) => Promise<void>;
logout: () => Promise<void>;
}
export const AuthContextKey: InjectionKey<AuthContext> = Symbol('auth');
// 提供 Context
export function useAuthProvider() {
const user = ref<User | null>(null);
const isAuthenticated = computed(() => !!user.value);
async function login(credentials: LoginCredentials) {
user.value = await authApi.login(credentials);
}
async function logout() {
await authApi.logout();
user.value = null;
}
const context: AuthContext = {
user,
isAuthenticated,
login,
logout,
};
provide(AuthContextKey, context);
return context;
}
// 使用 Context(子组件)
export function useAuth() {
const context = inject(AuthContextKey);
if (!context) {
throw new Error('useAuth() 必须在 AuthProvider 内调用');
}
return context;
}
// ===== 带泛型的 Context =====
// 通用 Context 创建工具
export function createContext<T>(key: InjectionKey<T>) {
const provideFn = (value: T) => provide(key, value);
const useFn = () => {
const context = inject(key);
if (!context) {
throw new Error(`Context ${key.toString()} 未找到`);
}
return context;
};
return { provide: provideFn, use: useFn };
}
// 使用
const { provide: provideUser, use: useUser } = createContext<UserContext>(Symbol('user'));
// ===== 多层注入与覆盖 =====
// 提供方
provide('app-version', '2.0');
// 子组件覆盖
provide('app-version', '2.1'); // 会覆盖父级的值
// 孙组件获取
const version = inject('app-version'); // 拿到 2.1
四、Pinia 状态管理
4.1 Store 定义
typescript
// ===== 定义 Store =====
// stores/user.ts
import { defineStore } from 'pinia';
import { ref, computed } from 'vue';
import type { User } from '@/types';
export const useUserStore = defineStore('user', () => {
// 状态
const user = ref<User | null>(null);
const token = ref<string | null>(localStorage.getItem('token'));
const isLoading = ref(false);
// 计算属性
const isAuthenticated = computed(() => !!token.value && !!user.value);
const userName = computed(() => user.value?.name ?? 'Guest');
const userAvatar = computed(() => user.value?.avatar ?? '/default-avatar.png');
// Actions
async function login(credentials: { email: string; password: string }) {
isLoading.value = true;
try {
const response = await api.post<{ user: User; token: string }>(
'/auth/login',
credentials
);
user.value = response.user;
token.value = response.token;
localStorage.setItem('token', response.token);
return response;
} finally {
isLoading.value = false;
}
}
async function logout() {
await api.post('/auth/logout');
user.value = null;
token.value = null;
localStorage.removeItem('token');
}
async function fetchUser() {
if (!token.value) return;
isLoading.value = true;
try {
user.value = await api.get<User>('/user/profile');
} finally {
isLoading.value = false;
}
}
function updateProfile(data: Partial<User>) {
if (user.value) {
user.value = { ...user.value, ...data };
}
}
return {
// 状态
user,
token,
isLoading,
// 计算属性
isAuthenticated,
userName,
userAvatar,
// Actions
login,
logout,
fetchUser,
updateProfile,
};
});
// ===== 跨 Store 调用 =====
import { useAuthStore } from './auth';
import { useCartStore } from './cart';
export const useOrderStore = defineStore('order', () => {
const authStore = useAuthStore(); // 调用其他 store
const cartStore = useCartStore();
async function createOrder() {
if (!authStore.isAuthenticated) {
throw new Error('请先登录');
}
const items = cartStore.items;
// 创建订单逻辑
const order = await api.post('/orders', {
userId: authStore.user!.id,
items,
});
cartStore.clear();
return order;
}
return { createOrder };
});
4.2 Pinia 插件与持久化
typescript
// ===== Pinia 插件:状态持久化 =====
// plugins/pinia-persist.ts
import { defineStore } from 'pinia';
import type { PiniaPluginContext } from 'pinia';
interface PersistOptions {
key?: string;
storage?: Storage;
paths?: string[]; // 只持久化这些路径
}
function createPersistPlugin(options: PersistOptions = {}) {
const { key = 'pinia', storage = localStorage, paths } = options;
return ({ store, options: storeOptions }: PiniaPluginContext) => {
// 检查 store 是否配置了 persist
const persist = (storeOptions as any).persist as PersistOptions | undefined;
if (!persist) return;
const persistKey = persist.key ?? `${key}-${store.$id}`;
const persistPaths = persist.paths ?? paths;
// 恢复状态
const stored = storage.getItem(persistKey);
if (stored) {
const parsed = JSON.parse(stored);
if (persistPaths) {
persistPaths.forEach((p) => {
(store.$state as any)[p] = parsed[p];
});
} else {
store.$patch(parsed);
}
}
// 监听变化,持久化
store.$subscribe(
(_, state) => {
const toStore = persistPaths
? Object.fromEntries(persistPaths.map(p => [p, (state as any)[p]]))
: state;
storage.setItem(persistKey, JSON.stringify(toStore));
},
{ detached: true }
);
};
}
// 注册插件
// main.ts
import { createPinia } from 'pinia';
const pinia = createPinia();
pinia.use(createPersistPlugin());
app.use(pinia);
// ===== Store 使用 persist =====
export const useSettingsStore = defineStore('settings', () => {
const theme = ref<'light' | 'dark'>('light');
const language = ref('zh-CN');
const compactMode = ref(false);
return { theme, language, compactMode };
}, {
// 开启持久化
persist: {
key: 'app-settings',
paths: ['theme', 'language'], // 只持久化 theme 和 language
},
});
4.3 Pinia 与 TypeScript
typescript
// ===== 类型安全的 Store =====
// stores/counter.ts
import { defineStore } from 'pinia';
import { ref, computed } from 'vue';
interface CounterState {
count: number;
lastUpdated: Date | null;
}
export const useCounterStore = defineStore('counter', () => {
// 状态 + 类型
const count = ref<number>(0);
const lastUpdated = ref<Date | null>(null);
// Getters(计算属性)
const doubleCount = computed(() => count.value * 2);
const isPositive = computed(() => count.value > 0);
// Actions
function increment(delta = 1) {
count.value += delta;
lastUpdated.value = new Date();
}
function decrement(delta = 1) {
count.value -= delta;
lastUpdated.value = new Date();
}
function reset() {
count.value = 0;
lastUpdated.value = null;
}
// $subscribe 返回状态快照的类型
function getState(): CounterState {
return {
count: count.value,
lastUpdated: lastUpdated.value,
};
}
return {
count,
lastUpdated,
doubleCount,
isPositive,
increment,
decrement,
reset,
getState,
};
});
// ===== 在组件中使用(类型自动推断)=====
const store = useCounterStore();
// count: Ref<number>
// doubleCount: ComputedRef<number>
// increment: (delta?: number) => void
store.increment(5);
store.count; // number(类型安全)
五、生命周期与副作用
5.1 生命周期钩子
typescript
// ===== 生命周期钩子对照表 =====
/*
Vue 3 Composition API Vue 2 Options API
───────────────────────────────────────────────
setup() beforeCreate, created
onMounted() mounted
onUpdated() updated
onUnmounted() unmounted
onBeforeMount() beforeMount
onBeforeUpdate() beforeUpdate
onBeforeUnmount() beforeUnmount
onErrorCaptured() errorCaptured
onRenderTracked() renderTracked(DevTools)
onRenderTriggered() renderTriggered(DevTools)
onServerPrefetch() serverPrefetch(SSR)
*/
// ===== 完整示例 =====
import {
onMounted,
onUnmounted,
onBeforeMount,
onUpdated,
onBeforeUpdate,
onErrorCaptured,
onRenderTracked,
onRenderTriggered,
} from 'vue';
export default {
setup() {
// 组件实例已创建,DOM 未挂载
onBeforeMount(() => {
console.log('onBeforeMount: DOM 即将挂载');
});
// DOM 已挂载
onMounted(() => {
console.log('onMounted: DOM 已挂载');
// 绑定事件(最佳位置)
window.addEventListener('resize', handleResize);
// 初始化第三方库
const chart = new Chart(canvasRef.value);
});
// DOM 更新前
onBeforeUpdate(() => {
console.log('onBeforeUpdate: DOM 即将更新');
});
// DOM 更新后
onUpdated(() => {
console.log('onUpdated: DOM 已更新');
});
// 组件卸载前(清理)
onUnmounted(() => {
console.log('onUnmounted: 组件即将卸载');
// 清理事件监听
window.removeEventListener('resize', handleResize);
// 销毁第三方实例
chart?.destroy();
// 清除定时器
clearInterval(timerId);
});
// 错误捕获
onErrorCaptured((err, instance, info) => {
console.error('Error:', err);
console.error('Component:', instance);
console.error('Info:', info);
// 返回 false 阻止错误传播
return false;
});
// 调试钩子
onRenderTracked(({ key, target, type }) => {
console.log('render tracked:', key, target, type);
});
onRenderTriggered(({ key, target, type }) => {
console.log('render triggered:', key, target, type);
});
return {};
},
};
// ===== 定时器 Composable(自动清理)=====
// composables/useInterval.ts
import { ref, onUnmounted } from 'vue';
export function useInterval(callback: () => void, interval: number) {
const isActive = ref(false);
let timerId: ReturnType<typeof setInterval> | null = null;
function start() {
if (!isActive.value) {
isActive.value = true;
timerId = setInterval(callback, interval);
}
}
function stop() {
if (isActive.value) {
isActive.value = false;
if (timerId) {
clearInterval(timerId);
timerId = null;
}
}
}
// 组件卸载时自动停止
onUnmounted(stop);
return {
isActive,
start,
stop,
restart: () => { stop(); start(); },
};
}
六、性能优化
6.1 虚拟滚动
typescript
// ===== 虚拟滚动列表 =====
// composables/useVirtualScroll.ts
import { ref, computed, onMounted, onUnmounted, type Ref } from 'vue';
export interface VirtualScrollOptions {
/** 数据源 */
items: Ref<any[]>;
/** 每项高度 */
itemHeight: number;
/** 可视区域高度 */
containerHeight: number;
/** 缓冲区数量 */
buffer?: number;
}
export function useVirtualScroll(options: VirtualScrollOptions) {
const { items, itemHeight, containerHeight, buffer = 3 } = options;
const scrollTop = ref(0);
const containerRef = ref<HTMLElement>();
// 可视区域能显示的项数
const visibleCount = Math.ceil(containerHeight / itemHeight);
// 起始索引
const startIndex = computed(() => {
return Math.max(0, Math.floor(scrollTop.value / itemHeight) - buffer);
});
// 结束索引
const endIndex = computed(() => {
return Math.min(
items.value.length - 1,
startIndex.value + visibleCount + buffer * 2
);
});
// 可见项
const visibleItems = computed(() => {
return items.value.slice(startIndex.value, endIndex.value + 1);
});
// 偏移量(用于定位起始项)
const offsetTop = computed(() => startIndex.value * itemHeight);
// 总高度(撑开滚动区域)
const totalHeight = computed(() => items.value.length * itemHeight);
function onScroll(e: Event) {
const target = e.target as HTMLElement;
scrollTop.value = target.scrollTop;
}
// 滚动到指定索引
function scrollToIndex(index: number) {
if (containerRef.value) {
containerRef.value.scrollTop = index * itemHeight;
}
}
return {
containerRef,
visibleItems,
offsetTop,
totalHeight,
startIndex,
onScroll,
scrollToIndex,
};
}
// ===== 使用虚拟滚动 =====
const allUsers = ref<User[]>([]);
const {
containerRef,
visibleItems,
offsetTop,
totalHeight,
scrollToIndex,
} = useVirtualScroll({
items: allUsers,
itemHeight: 60,
containerHeight: 500,
});
// 加载 10000 条数据测试
onMounted(async () => {
allUsers.value = await fetchUsers(10000);
});
6.2 懒加载与异步组件
vue
<!-- ===== Suspense 异步组件 ===== -->
<template>
<Suspense>
<!-- 主要内容(异步)-->
<template #default>
<AsyncUserProfile :user-id="userId" />
</template>
<!-- 加载中 -->
<template #fallback>
<div class="loading-skeleton">
<div class="skeleton-avatar"></div>
<div class="skeleton-text"></div>
</div>
</template>
</Suspense>
</template>
<!-- ===== 异步组件定义 ===== -->
<script setup lang="ts">
import { defineAsyncComponent, onMounted } from 'vue';
const AsyncUserProfile = defineAsyncComponent({
loader: () => import('./UserProfile.vue'),
loadingComponent: LoadingSpinner,
errorComponent: ErrorBoundary,
delay: 200, // 延迟显示 loading
timeout: 3000, // 超时显示 error
suspensible: false, // 是否受 Suspense 控制
});
</script>
<!-- ===== 路由懒加载 ===== -->
<script setup lang="ts">
import { defineAsyncComponent } from 'vue';
import type { RouteRecordRaw } from 'vue-router';
const routes: RouteRecordRaw[] = [
{
path: '/dashboard',
component: () => import('./views/Dashboard.vue'), // 懒加载
},
{
path: '/users',
component: () => import(/* webpackChunkName: "users" */ './views/Users.vue'),
},
{
path: '/settings',
component: () => import(
/* webpackChunkName: "settings" */
/* webpackPrefetch: true */
'./views/Settings.vue'
),
},
];
</script>
七、Vue 3 + TypeScript 最佳实践
7.1 类型定义
typescript
// ===== Vue 3 TypeScript 类型 =====
import type { Ref, ComputedRef } from 'vue';
// Props 定义
interface Props {
title: string;
count?: number;
items: string[];
onClick?: (id: number) => void;
}
const props = withDefaults(defineProps<Props>(), {
count: 0,
items: () => [],
});
// Emits 定义
const emit = defineEmits<{
(e: 'update', value: string): void;
(e: 'delete', id: number): void;
(e: 'click', event: MouseEvent): void;
}>();
// 模板中调用
emit('update', 'new value');
// Expose 给父组件
defineExpose({
open: () => void,
close: () => void,
value: props.title,
});
// Refs
const canvasRef = ref<HTMLCanvasElement>();
const inputRef = ref<InstanceType<typeof import('element-plus').ElInput>>();
// ===== 泛型组件 =====
// GenericList.vue
<script setup lang="ts" generic="T extends { id: string | number }">
import { computed } from 'vue';
const props = defineProps<{
items: T[];
renderItem: (item: T) => any;
}>();
const renderedItems = computed(() => props.items.map(props.renderItem));
</script>
<!-- 使用 -->
<GenericList :items="users" :render-item="(u) => u.name" />
<GenericList :items="products" :render-item="(p) => p.title" />
7.2 Vite 配置
typescript
// vite.config.ts
import { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue';
import { resolve } from 'path';
export default defineConfig({
plugins: [vue()],
resolve: {
alias: {
'@': resolve(__dirname, 'src'),
},
},
// 生产构建优化
build: {
target: 'es2020',
cssCodeSplit: true,
rollupOptions: {
output: {
// 代码分割
manualChunks: {
'vue-vendor': ['vue', 'vue-router', 'pinia'],
'ui-vendor': ['element-plus'],
},
},
},
// 压缩
minify: 'terser',
terserOptions: {
compress: {
drop_console: true,
drop_debugger: true,
},
},
},
// 开发服务器
server: {
port: 3000,
proxy: {
'/api': {
target: 'http://localhost:8080',
changeOrigin: true,
},
},
},
// 优化依赖
optimizeDeps: {
include: ['vue', 'vue-router', 'pinia', 'axios'],
},
});
八、总结
技术全景
| 层 | 核心概念 | 关键点 |
|---|---|---|
| 响应式 | ref/reactive/Proxy | 模板自动解包,Proxy 深度代理 |
| Composable | 逻辑复用 | useCounter/useFetch/usePagination |
| 依赖注入 | provide/inject | Symbol key + 响应式传递 |
| Pinia | 状态管理 | Setup Store + 插件 + 持久化 |
| 生命周期 | onMounted/onUnmounted | 自动清理副作用 |
| 性能 | 虚拟滚动 + 懒加载 | 大列表 + 异步组件 |
| TypeScript | Generics + defineProps | 泛型组件 + 类型推导 |
Composable 设计原则
| 原则 | 说明 |
|---|---|
| 单一职责 | 每个 Composable 只做一件事 |
| 命名规范 | use 前缀:useXxx |
| 返回响应式 | 返回 ref/reactive,非原始值 |
| 自动清理 | onUnmounted 清理定时器/监听器 |
| SSR 安全 | 浏览器 API 用 onMounted 包裹 |
| 类型安全 | 泛型定义 props/emits |
Composition API vs Options API
| 场景 | 推荐 | 原因 |
|---|---|---|
| 新项目 | Composition API | 更好的类型推导、逻辑复用、性能 |
| 简单页面 | Options API 可用 | 直观,逻辑少时无问题 |
| 复杂逻辑组件 | Composition API | 逻辑分组,避免选项分散 |
| 逻辑复用 | Composition API | Composables vs Mixins(无冲突) |
| 大型项目 | Composition API | 更好的 Tree-shaking |
本文涵盖 Vue 3 Composition API 完整知识:响应式原理(Proxy + 依赖追踪)+ ref/reactive/computed/watch + Composable 设计模式(useCounter/useLocalStorage/useDebounce/usePagination/useFetch)+ 依赖注入 provide/inject + Pinia Setup Store + 持久化插件 + 生命周期钩子 + 性能优化(虚拟滚动 + 懒加载)+ TypeScript 泛型组件 + Vite 构建优化。