Vue3 Composition API
2026/3/20大约 8 分钟
Vue3 Composition API 完全指南:重新定义逻辑复用
Composition API 是 Vue3 最具革命性的特性。它不仅仅是一种新的写法,更是一种全新的代码组织和复用方式。本文将深入探讨 Composition API 的方方面面,帮你掌握这一强大工具。
为什么需要 Composition API?
Options API 的痛点
// Options API:相关逻辑被强制分散
export default {
data() {
return {
// 功能 A 的数据
userList: [],
userLoading: false,
// 功能 B 的数据
searchQuery: "",
searchResults: [],
};
},
computed: {
// 功能 A 的计算属性
activeUsers() {
/* ... */
},
// 功能 B 的计算属性
filteredResults() {
/* ... */
},
},
methods: {
// 功能 A 的方法
fetchUsers() {
/* ... */
},
deleteUser() {
/* ... */
},
// 功能 B 的方法
search() {
/* ... */
},
clearSearch() {
/* ... */
},
},
mounted() {
// 功能 A 的初始化
this.fetchUsers();
// 功能 B 的初始化
this.initSearch();
},
};
Composition API 的优势
// Composition API:相关逻辑可以组织在一起
import { useUsers } from "./composables/useUsers";
import { useSearch } from "./composables/useSearch";
export default {
setup() {
// 功能 A:用户管理(所有相关逻辑在一起)
const { userList, userLoading, activeUsers, fetchUsers, deleteUser } =
useUsers();
// 功能 B:搜索功能(所有相关逻辑在一起)
const { searchQuery, searchResults, filteredResults, search, clearSearch } =
useSearch();
return {
userList,
userLoading,
activeUsers,
fetchUsers,
deleteUser,
searchQuery,
searchResults,
filteredResults,
search,
clearSearch,
};
},
};
核心 API 详解
ref 与 reactive
import { ref, reactive, toRefs, toRef } from "vue";
// ref:用于基本类型(也可用于对象)
const count = ref(0);
const message = ref("Hello");
const user = ref({ name: "张三" });
// 访问/修改需要 .value
console.log(count.value); // 0
count.value++;
user.value.name = "李四";
// reactive:用于对象
const state = reactive({
count: 0,
user: { name: "张三" },
});
// 直接访问,不需要 .value
console.log(state.count); // 0
state.count++;
// reactive 的局限性
const state = reactive({ count: 0 });
let { count } = state; // ❌ 解构后失去响应性
let count = toRef(state, "count"); // ✅ 保持响应性
let { count } = toRefs(state); // ✅ 批量转换
computed
import { ref, computed } from "vue";
const firstName = ref("张");
const lastName = ref("三");
// 只读计算属性
const fullName = computed(() => firstName.value + lastName.value);
// 可写计算属性
const fullName = computed({
get() {
return firstName.value + lastName.value;
},
set(newValue) {
[firstName.value, lastName.value] = newValue.split(" ");
},
});
// 使用
console.log(fullName.value); // '张三'
fullName.value = "李 四"; // firstName = '李', lastName = '四'
watch 与 watchEffect
import { ref, reactive, watch, watchEffect, watchPostEffect } from "vue";
const count = ref(0);
const state = reactive({ name: "张三", age: 25 });
// 监听单个 ref
watch(count, (newVal, oldVal) => {
console.log(`count: ${oldVal} → ${newVal}`);
});
// 监听 reactive 对象的属性
watch(
() => state.name,
(newVal, oldVal) => {
console.log(`name: ${oldVal} → ${newVal}`);
}
);
// 监听多个来源
watch([count, () => state.name], ([newCount, newName], [oldCount, oldName]) => {
console.log("count or name changed");
});
// 配置选项
watch(count, callback, {
immediate: true, // 立即执行一次
deep: true, // 深度监听
flush: "post", // 在 DOM 更新后执行
once: true, // 只执行一次(Vue 3.4+)
});
// watchEffect:自动追踪依赖
const stop = watchEffect(() => {
// 自动追踪 count.value 和 state.name
console.log(`count: ${count.value}, name: ${state.name}`);
});
// 停止监听
stop();
// 清理副作用
watchEffect((onCleanup) => {
const controller = new AbortController();
fetch("/api/data", { signal: controller.signal })
.then((res) => res.json())
.then((data) => {
// 处理数据
});
onCleanup(() => {
controller.abort();
});
});
// watchPostEffect:在 DOM 更新后执行(相当于 flush: 'post')
watchPostEffect(() => {
// 可以安全访问更新后的 DOM
});
生命周期钩子
import {
onBeforeMount,
onMounted,
onBeforeUpdate,
onUpdated,
onBeforeUnmount,
onUnmounted,
onActivated,
onDeactivated,
onErrorCaptured,
onRenderTracked, // 开发环境调试
onRenderTriggered, // 开发环境调试
} from "vue";
// 可以多次调用
onMounted(() => {
console.log("mounted 1");
});
onMounted(() => {
console.log("mounted 2");
});
// 在组合式函数中使用
function useEventListener(target, event, callback) {
onMounted(() => {
target.addEventListener(event, callback);
});
onUnmounted(() => {
target.removeEventListener(event, callback);
});
}
provide 与 inject
import { provide, inject, ref, readonly } from "vue";
// 祖先组件
const theme = ref("dark");
provide("theme", readonly(theme)); // 提供只读引用
provide("updateTheme", (newTheme) => {
theme.value = newTheme;
});
// 后代组件
const theme = inject("theme");
const updateTheme = inject("updateTheme");
// 带默认值
const theme = inject("theme", "light");
// 工厂函数默认值
const config = inject("config", () => ({ timeout: 3000 }), true);
组合式函数(Composables)
组合式函数是 Composition API 的灵魂,它让逻辑复用变得优雅而强大。
基础示例:useMouse
// composables/useMouse.js
import { ref, onMounted, onUnmounted } from "vue";
export function useMouse() {
const x = ref(0);
const y = ref(0);
function update(event) {
x.value = event.pageX;
y.value = event.pageY;
}
onMounted(() => {
window.addEventListener("mousemove", update);
});
onUnmounted(() => {
window.removeEventListener("mousemove", update);
});
return { x, y };
}
// 使用
import { useMouse } from "@/composables/useMouse";
const { x, y } = useMouse();
实用示例:useLocalStorage
// composables/useLocalStorage.js
import { ref, watch } from "vue";
export function useLocalStorage(key, defaultValue) {
// 从 localStorage 读取初始值
const storedValue = localStorage.getItem(key);
const data = ref(storedValue ? JSON.parse(storedValue) : defaultValue);
// 监听变化,自动保存
watch(
data,
(newValue) => {
if (newValue === null || newValue === undefined) {
localStorage.removeItem(key);
} else {
localStorage.setItem(key, JSON.stringify(newValue));
}
},
{ deep: true }
);
return data;
}
// 使用
const settings = useLocalStorage("app-settings", {
theme: "light",
language: "zh-CN",
});
实用示例:useFetch
// composables/useFetch.js
import { ref, watchEffect, toValue } from "vue";
export function useFetch(url) {
const data = ref(null);
const error = ref(null);
const loading = ref(false);
async function fetchData() {
loading.value = true;
error.value = null;
try {
const response = await fetch(toValue(url));
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
data.value = await response.json();
} catch (e) {
error.value = e.message;
} finally {
loading.value = false;
}
}
// 如果 url 是响应式的,自动重新请求
watchEffect(() => {
fetchData();
});
return { data, error, loading, refetch: fetchData };
}
// 使用
const { data: users, loading, error, refetch } = useFetch("/api/users");
// 响应式 URL
const userId = ref(1);
const { data: user } = useFetch(() => `/api/users/${userId.value}`);
// userId 变化时自动重新请求
实用示例:useDebounce
// composables/useDebounce.js
import { ref, watch } from "vue";
export function useDebounce(value, delay = 300) {
const debouncedValue = ref(value.value);
let timer = null;
watch(value, (newValue) => {
if (timer) clearTimeout(timer);
timer = setTimeout(() => {
debouncedValue.value = newValue;
}, delay);
});
return debouncedValue;
}
// 使用
const searchQuery = ref("");
const debouncedQuery = useDebounce(searchQuery, 500);
watch(debouncedQuery, (query) => {
// 防抖后的搜索
search(query);
});
实用示例:useAsync
// composables/useAsync.js
import { ref, computed } from "vue";
export function useAsync(asyncFunction) {
const data = ref(null);
const error = ref(null);
const loading = ref(false);
const isSuccess = computed(
() => !loading.value && !error.value && data.value !== null
);
const isError = computed(() => !loading.value && error.value !== null);
async function execute(...args) {
loading.value = true;
error.value = null;
try {
data.value = await asyncFunction(...args);
return data.value;
} catch (e) {
error.value = e;
throw e;
} finally {
loading.value = false;
}
}
function reset() {
data.value = null;
error.value = null;
loading.value = false;
}
return {
data,
error,
loading,
isSuccess,
isError,
execute,
reset,
};
}
// 使用
const { data, loading, error, execute } = useAsync(async (id) => {
const response = await fetch(`/api/users/${id}`);
return response.json();
});
// 手动触发
await execute(1);
实用示例:useEventListener
// composables/useEventListener.js
import { onMounted, onUnmounted, unref } from "vue";
export function useEventListener(target, event, callback, options) {
onMounted(() => {
const el = unref(target);
el?.addEventListener(event, callback, options);
});
onUnmounted(() => {
const el = unref(target);
el?.removeEventListener(event, callback, options);
});
}
// 使用
const buttonRef = ref(null);
useEventListener(buttonRef, "click", () => {
console.log("clicked");
});
useEventListener(window, "resize", () => {
console.log("resized");
});
实用示例:useIntersectionObserver
// composables/useIntersectionObserver.js
import { ref, onMounted, onUnmounted } from "vue";
export function useIntersectionObserver(elementRef, options = {}) {
const isVisible = ref(false);
let observer = null;
onMounted(() => {
observer = new IntersectionObserver(([entry]) => {
isVisible.value = entry.isIntersecting;
}, options);
if (elementRef.value) {
observer.observe(elementRef.value);
}
});
onUnmounted(() => {
observer?.disconnect();
});
return { isVisible };
}
// 使用:懒加载
const imageRef = ref(null);
const { isVisible } = useIntersectionObserver(imageRef);
// 模板中
// <img ref="imageRef" v-if="isVisible" :src="imageSrc" />
实用示例:useForm
// composables/useForm.js
import { reactive, computed } from "vue";
export function useForm(initialValues, validationRules = {}) {
const form = reactive({ ...initialValues });
const errors = reactive({});
const touched = reactive({});
// 验证单个字段
function validateField(field) {
const rules = validationRules[field];
if (!rules) return true;
for (const rule of rules) {
const result = rule(form[field]);
if (result !== true) {
errors[field] = result;
return false;
}
}
errors[field] = null;
return true;
}
// 验证所有字段
function validate() {
let isValid = true;
for (const field in validationRules) {
if (!validateField(field)) {
isValid = false;
}
}
return isValid;
}
// 设置字段为已触摸
function touch(field) {
touched[field] = true;
validateField(field);
}
// 重置表单
function reset() {
Object.assign(form, initialValues);
Object.keys(errors).forEach((key) => (errors[key] = null));
Object.keys(touched).forEach((key) => (touched[key] = false));
}
const isValid = computed(() => {
return Object.values(errors).every((e) => !e);
});
return {
form,
errors,
touched,
isValid,
validate,
validateField,
touch,
reset,
};
}
// 使用
const { form, errors, touched, isValid, validate, touch, reset } = useForm(
{
username: "",
email: "",
password: "",
},
{
username: [
(v) => !!v || "用户名不能为空",
(v) => v.length >= 3 || "用户名至少3个字符",
],
email: [
(v) => !!v || "邮箱不能为空",
(v) => /^\S+@\S+\.\S+$/.test(v) || "邮箱格式不正确",
],
password: [
(v) => !!v || "密码不能为空",
(v) => v.length >= 6 || "密码至少6个字符",
],
}
);
组合式函数最佳实践
1. 命名约定
// 以 use 开头
useCounter();
useFetch();
useLocalStorage();
// 返回对象包含所有暴露的值
const { count, increment, decrement } = useCounter();
2. 参数处理
// 支持响应式参数
import { toValue, watchEffect } from "vue";
export function useFetch(url) {
watchEffect(() => {
// toValue 会处理 ref、getter、普通值
const urlValue = toValue(url);
fetch(urlValue);
});
}
// 使用时可以传递不同类型
useFetch("/api/users"); // 字符串
useFetch(urlRef); // ref
useFetch(() => `/api/users/${id.value}`); // getter
3. 返回值设计
// 返回 ref 而非 reactive,便于解构
export function useCounter() {
const count = ref(0);
function increment() {
count.value++;
}
// ✅ 返回对象包含 ref
return { count, increment };
}
// 使用时可以直接解构
const { count, increment } = useCounter();
4. 副作用清理
export function useInterval(callback, interval) {
const timer = ref(null);
function start() {
stop();
timer.value = setInterval(callback, interval);
}
function stop() {
if (timer.value) {
clearInterval(timer.value);
timer.value = null;
}
}
// 自动清理
onMounted(start);
onUnmounted(stop);
return { start, stop };
}
5. TypeScript 支持
// composables/useCounter.ts
import { ref, computed, Ref } from "vue";
interface UseCounterOptions {
min?: number;
max?: number;
}
interface UseCounterReturn {
count: Ref<number>;
doubleCount: Ref<number>;
increment: () => void;
decrement: () => void;
reset: () => void;
}
export function useCounter(
initialValue: number = 0,
options: UseCounterOptions = {}
): UseCounterReturn {
const { min = -Infinity, max = Infinity } = options;
const count = ref(initialValue);
const doubleCount = computed(() => count.value * 2);
function increment() {
if (count.value < max) {
count.value++;
}
}
function decrement() {
if (count.value > min) {
count.value--;
}
}
function reset() {
count.value = initialValue;
}
return {
count,
doubleCount,
increment,
decrement,
reset,
};
}
与 Options API 对比
代码组织
// Options API:按选项类型组织
export default {
data() { /* 所有数据 */ },
computed: { /* 所有计算属性 */ },
methods: { /* 所有方法 */ },
mounted() { /* 所有初始化逻辑 */ }
}
// Composition API:按功能组织
export default {
setup() {
// 功能 A
const { dataA, methodA } = useFeatureA()
// 功能 B
const { dataB, methodB } = useFeatureB()
return { dataA, methodA, dataB, methodB }
}
}
逻辑复用
// Options API:mixin(有命名冲突风险)
const myMixin = {
data() {
return { count: 0 };
},
methods: {
increment() {
this.count++;
},
},
};
export default {
mixins: [myMixin],
};
// Composition API:组合式函数(无冲突风险)
const { count, increment } = useCounter();
const { count: count2, increment: increment2 } = useCounter();
// 可以重命名,完全避免冲突
总结
Composition API 带来了全新的代码组织方式:
- 响应式 API:ref、reactive、computed、watch 等
- 生命周期钩子:onMounted、onUnmounted 等
- 依赖注入:provide、inject
- 组合式函数:逻辑复用的最佳方式
掌握 Composition API 后,你将能够:
- 更好地组织复杂组件的代码
- 创建可复用的逻辑单元
- 享受更好的 TypeScript 支持
- 编写更易测试的代码
推荐使用 VueUse 库(https://vueuse.org/),它提供了大量实用的组合式函数,可以直接在项目中使用。