Vue3 Composition API 进阶
大约 7 分钟约 2150 字
Vue3 Composition API 进阶
简介
Vue3 Composition API 进阶涵盖自定义 Composable、响应式原理、effectScope 和性能优化。掌握 customRef(自定义响应式引用)、shallowRef/shallowReactive(浅层响应式)、effectScope(副作用作用域管理)和 computed 缓存策略,能够编写更优雅和高效的 Vue3 应用。Composable 是 Composition API 的核心复用模式,通过 use 前缀命名的函数封装可复用的有状态逻辑,替代 Vue2 的 Mixins。
特点
实现
高级 Composable 模式
// ========== useDebouncedRef — 防抖响应式引用 ==========
import { customRef, type Ref } from 'vue';
function useDebouncedRef<T>(value: T, delay = 300): Ref<T> {
let timeout: ReturnType<typeof setTimeout>;
return customRef((track, trigger) => {
return {
get() {
track(); // 依赖追踪
return value;
},
set(newValue: T) {
clearTimeout(timeout);
timeout = setTimeout(() => {
value = newValue;
trigger(); // 触发更新
}, delay);
},
};
});
}
// 使用
const search = useDebouncedRef('', 300);
search.value = 'keyword'; // 300ms 后才触发更新// ========== useLocalStorage — 本地存储响应式 ==========
import { ref, watch, type Ref } from 'vue';
function useLocalStorage<T>(key: string, defaultValue: T): Ref<T> {
const stored = localStorage.getItem(key);
const data = ref<T>(stored ? JSON.parse(stored) : defaultValue) as Ref<T>;
watch(data, (newValue) => {
localStorage.setItem(key, JSON.stringify(newValue));
}, { deep: true });
return data;
}
// 使用
const theme = useLocalStorage('theme', 'light');
const userSettings = useLocalStorage('settings', { fontSize: 14, language: 'zh' });// ========== useAsync — 异步数据请求 ==========
import { ref, readonly, type Ref } from 'vue';
interface AsyncState<T> {
data: Ref<T | null>;
error: Ref<Error | null>;
loading: Ref<boolean>;
refresh: () => Promise<void>;
}
function useAsync<T>(fn: () => Promise<T>, immediate = true): AsyncState<T> {
const data = ref<T | null>(null) as Ref<T | null>;
const error = ref<Error | null>(null);
const loading = ref(false);
async function execute() {
loading.value = true;
error.value = null;
try {
data.value = await fn();
} catch (e) {
error.value = e as Error;
} finally {
loading.value = false;
}
}
if (immediate) execute();
return { data: readonly(data), error: readonly(error), loading: readonly(loading), refresh: execute };
}
// 使用
const { data: devices, loading, error, refresh } = useAsync(() => fetchDevices());
// 带参数的异步请求
function useFetch<T>(url: Ref<string>) {
return useAsync(async () => {
const res = await fetch(url.value);
return res.json() as Promise<T>;
});
}// ========== usePagination — 分页 ==========
import { ref, computed, type Ref } from 'vue';
function usePagination(fetchFn: (page: number, pageSize: number) => Promise<any>, pageSize = 20) {
const current = ref(1);
const total = ref(0);
const data = ref<any[]>([]);
const loading = ref(false);
const totalPages = computed(() => Math.ceil(total.value / pageSize));
const hasMore = computed(() => current.value < totalPages.value);
async function load() {
loading.value = true;
try {
const result = await fetchFn(current.value, pageSize);
data.value = result.items;
total.value = result.total;
} finally {
loading.value = false;
}
}
function goTo(page: number) {
current.value = page;
load();
}
function next() {
if (hasMore.value) goTo(current.value + 1);
}
function prev() {
if (current.value > 1) goTo(current.value - 1);
}
// 初始加载
load();
return { current, total, totalPages, hasMore, data, loading, load, goTo, next, prev };
}effectScope 与生命周期管理
import { effectScope, onScopeDispose, ref, readonly, computed, watch } from 'vue';
// ========== effectScope — 自动管理副作用生命周期 ==========
function useMousePosition() {
const scope = effectScope(true); // 独立作用域
const pos = scope.run(() => {
const x = ref(0);
const y = ref(0);
const handler = (e: MouseEvent) => {
x.value = e.clientX;
y.value = e.clientY;
};
window.addEventListener('mousemove', handler);
// 作用域销毁时自动清理
onScopeDispose(() => {
window.removeEventListener('mousemove', handler);
});
return {
x: readonly(x),
y: readonly(y),
};
})!;
// 手动停止作用域
const stop = () => scope.stop();
return { ...pos, stop };
}
// 使用
const { x, y } = useMousePosition();// ========== useWebSocket — WebSocket Composable ==========
import { ref, readonly, effectScope, onScopeDispose } from 'vue';
type WsStatus = 'connecting' | 'open' | 'closed' | 'error';
function useWebSocket(url: string) {
const data = ref<any>(null);
const status = ref<WsStatus>('connecting');
let ws: WebSocket | null = null;
let reconnectTimer: ReturnType<typeof setTimeout>;
const messageQueue: any[] = [];
const scope = effectScope(true);
scope.run(() => {
function connect() {
status.value = 'connecting';
try {
ws = new WebSocket(url);
ws.onopen = () => {
status.value = 'open';
// 发送排队的消息
while (messageQueue.length > 0) {
ws!.send(JSON.stringify(messageQueue.shift()));
}
};
ws.onmessage = (e) => {
try {
data.value = JSON.parse(e.data);
} catch {
data.value = e.data;
}
};
ws.onclose = () => {
status.value = 'closed';
// 自动重连
reconnectTimer = setTimeout(connect, 3000);
};
ws.onerror = () => {
status.value = 'error';
};
} catch {
status.value = 'error';
}
}
connect();
onScopeDispose(() => {
clearTimeout(reconnectTimer);
ws?.close();
});
});
function send(msg: any) {
if (status.value === 'open' && ws) {
ws.send(JSON.stringify(msg));
} else {
messageQueue.push(msg); // 排队等待连接
}
}
function disconnect() {
clearTimeout(reconnectTimer);
ws?.close();
scope.stop();
}
return {
data: readonly(data),
status: readonly(status),
send,
disconnect,
};
}
// 使用
const { data, status, send, disconnect } = useWebSocket('ws://localhost:8080/ws');
// 发送消息
send({ type: 'subscribe', channel: 'devices' });// ========== useEventListener — 事件监听 ==========
import { onMounted, onUnmounted, type Ref } from 'vue';
function useEventListener<K extends keyof WindowEventMap>(
target: Ref<HTMLElement | Window | Document> | HTMLElement | Window | Document,
event: K,
handler: (event: WindowEventMap[K]) => void,
options?: boolean | AddEventListenerOptions
) {
onMounted(() => {
const el = target instanceof HTMLElement ? target : target.value || window;
el.addEventListener(event, handler as EventListener, options);
});
onUnmounted(() => {
const el = target instanceof HTMLElement ? target : target.value || window;
el.removeEventListener(event, handler as EventListener, options);
});
}
// 使用
const buttonRef = ref<HTMLElement>();
useEventListener(buttonRef, 'click', (e) => {
console.log('Button clicked:', e);
});
useEventListener(window, 'resize', () => {
console.log('Window resized');
});性能优化模式
// ========== shallowRef — 大对象只追踪引用变化 ==========
import { shallowRef, triggerRef } from 'vue';
// 大列表 — 不需要深层响应式
const bigList = shallowRef<Item[]>([]);
// 更新时需要重新赋值(触发响应式)
function addItem(item: Item) {
bigList.value = [...bigList.value, item]; // 创建新数组
}
// 或者手动触发更新
function modifyItem(index: number, updates: Partial<Item>) {
bigList.value[index] = { ...bigList.value[index], ...updates };
triggerRef(bigList); // 手动触发
}
// ========== shallowReactive — 只追踪第一层属性 ==========
const config = shallowReactive({
theme: 'light',
locale: 'zh-CN',
settings: { fontSize: 14, padding: 16 }, // settings 不追踪
});
// 修改第一层属性 — 触发更新
config.theme = 'dark'; // OK
// 修改深层属性 — 不触发更新
config.settings.fontSize = 16; // 不会触发更新
// ========== markRaw — 阻止对象变为响应式 ==========
import { markRaw } from 'vue';
// 大型第三方库实例不需要响应式
const chartInstance = markRaw(new ECharts(container));
// 富对象数据
const formData = markRaw({
files: [/* File 对象不需要响应式 */],
});// ========== computed 缓存策略 ==========
import { computed, ref } from 'vue';
const items = ref<Item[]>([...]);
// computed — 只在依赖变化时重新计算,有缓存
const filteredList = computed(() => {
console.log('重新计算 filteredList');
return items.value.filter(item => item.active);
});
const sortedList = computed(() => {
console.log('重新计算 sortedList');
return [...filteredList.value].sort((a, b) => a.name.localeCompare(b.name));
});
// items 变化 → filteredList 重新计算 → sortedList 重新计算
// 但如果 items 没变,多次访问 filteredList 不会重新计算(缓存)
// ========== 虚拟滚动 Composable ==========
function useVirtualScroll(options: {
itemHeight: number;
containerHeight: number;
totalItems: number;
}) {
const scrollTop = ref(0);
const visibleCount = Math.ceil(options.containerHeight / options.itemHeight);
const startIndex = computed(() =>
Math.max(0, Math.floor(scrollTop.value / options.itemHeight) - 2) // 多渲染 2 个
);
const endIndex = computed(() =>
Math.min(options.totalItems, startIndex.value + visibleCount + 4)
);
const totalHeight = computed(() => options.itemHeight * options.totalItems);
const offsetY = computed(() => startIndex.value * options.itemHeight);
function onScroll(e: Event) {
scrollTop.value = (e.target as HTMLElement).scrollTop;
}
return { startIndex, endIndex, totalHeight, offsetY, onScroll };
}依赖注入模式
// ========== provide/inject 跨组件通信 ==========
import { provide, inject, type InjectionKey, readonly } from 'vue';
// 定义注入 Key(类型安全)
export const AUTH_KEY: InjectionKey<ReturnType<typeof createAuth>> = Symbol('auth');
export const NOTIFICATION_KEY: InjectionKey<NotificationService> = Symbol('notification');
// 插件中 provide
const auth = createAuth();
provide(AUTH_KEY, auth);
// 组件中 inject
function useAuth() {
const auth = inject(AUTH_KEY);
if (!auth) throw new Error('Auth 未注册');
return auth;
}
// 只读注入
function useAuthReadonly() {
const auth = inject(AUTH_KEY);
if (!auth) throw new Error('Auth 未注册');
return {
user: readonly(auth.user),
isAuthenticated: readonly(auth.isAuthenticated),
login: auth.login,
logout: auth.logout,
};
}优点
缺点
总结
Composition API 进阶重点在于 Composable 的设计模式和 effectScope 的生命周期管理。customRef 实现自定义响应式行为(如防抖 ref)。shallowRef 用于大对象性能优化,避免深层代理开销。effectScope 将相关副作用收集在一起,支持批量停止。建议遵循 use 前缀命名规范,按功能拆分 Composable,为每个 Composable 编写文档和单元测试。
关键知识点
- customRef 允许自定义 track/trigger 控制响应式触发时机。
- effectScope 将相关副作用收集在一起,支持批量停止。
- shallowRef 只追踪 .value 的变化,不深度递归。
- onScopeDispose 在作用域销毁时执行清理逻辑。
- toRefs 将 reactive 对象的每个属性转为 ref,保持响应性。
- toRaw 获取响应式对象的原始对象(非代理)。
项目落地视角
- 封装项目级 Composable 库(useAuth、usePermission、useDevice)。
- 使用 effectScope 管理全局事件监听的生命周期。
- 大列表使用 shallowRef + 虚拟滚动优化性能。
常见误区
- 解构 ref/reactive 丢失响应性(需使用 toRefs)。
- 在 Composable 中直接操作 DOM 而非使用模板 ref。
- 忘记清理定时器和事件监听导致内存泄漏。
进阶路线
- 学习 Vue3 响应式系统源码(Proxy + effect + track/trigger)。
- 研究 VueUse 库的 Composable 实现模式。
- 了解 Vue3.4+ 的响应式优化(浅层响应式改进)。
适用场景
- 需要跨组件复用的逻辑(如设备连接、权限检查)。
- 需要自动清理的副作用(如 WebSocket、定时器)。
- 大数据列表的性能优化。
落地建议
- 建立 composables 目录统一管理。
- 遵循 useXxx 命名规范,返回 ref 和方法。
- 为每个 Composable 编写单元测试。
排错清单
- 检查 ref 是否被意外解构(应使用 toRefs)。
- 确认 shallowRef 的更新方式(重新赋值而非 .push)。
- 检查 effectScope 是否正确清理副作用。
复盘问题
- Composable 和 Vuex/Pinia 在状态管理上各适合什么场景?
- 如何避免 Composable 之间的循环依赖?
- effectScope 的使用是否覆盖了所有需要清理的副作用?
