𝑻𝒆𝒏𝑪𝒍𝒂𝒘正在头脑风暴···
𝑻𝒆𝒏𝑲𝒊𝑺𝒆𝒀𝒂の𝑨𝒈𝒆𝒏𝒕助手
𝑻𝒆𝒏-𝒇𝒍𝒂𝒔𝒉

Vue 3 Composition API 完全指南

Vue 3 的发布带来了革命性的变化,其中最引人注目的莫过于 Composition API 的引入。这个全新的 API 设计理念让我们能够更好地组织代码、复用逻辑,同时保持代码的可读性和可维护性。本文将深入浅出地讲解 Vue 3 Composition API 的各个方面,帮助你从零开始掌握这一强大的前端开发工具。

一、Composition API 的诞生背景

1.1 Options API 的局限性

在 Vue 2 时代,我们使用的是 Options API:

export default {
data() {
return {
count: 0,
name: '张三'
};
},
computed: {
doubleCount() {
return this.count * 2;
}
},
methods: {
increment() {
this.count++;
}
},
mounted() {
console.log('组件已挂载');
}
};

虽然 Options API 在简单场景下很好用,但随着项目规模扩大,会出现以下问题:

  1. 相关逻辑分散:同一个功能的代码分散在 data、computed、methods、mounted 等不同的选项中
  2. 代码复用困难:抽取可复用逻辑需要使用 mixins,但可能导致命名冲突和逻辑不清晰
  3. TypeScript 支持不友好:类型推断能力有限
  4. 代码可读性下降:大型组件中难以快速定位相关逻辑

1.2 Vue 3 的解决方案

Vue 3 引入了 Composition API,通过 setup() 函数让相关逻辑可以集中在一起:

import { ref, computed, onMounted } from 'vue';

export default {
setup() {
const count = ref(0);
const name = ref('张三');

const doubleCount = computed(() => count.value * 2);

const increment = () => {
count.value++;
};

onMounted(() => {
console.log('组件已挂载');
});

return {
count,
name,
doubleCount,
increment
};
}
};

二、核心 API 详解

2.1 ref 和 reactive

refreactive 是 Vue 3 中创建响应式数据的核心方法。

ref:用于基本类型

import { ref } from 'vue';

// 创建 ref
const count = ref(0);
const message = ref('Hello Vue 3');
const isActive = ref(false);

// 访问和修改值
console.log(count.value); // 0
count.value = 1;
console.log(count.value); // 1

重要提示:在模板中使用时,ref 会自动解包,不需要加 .value

<template>
<div>{{ count }}</div>
<button @click="increment">增加</button>
</template>

<script>
import { ref } from 'vue';

export default {
setup() {
const count = ref(0);

const increment = () => {
count.value++;
};

return { count, increment };
}
};
</script>

在 JavaScript 代码中,必须使用 .value 访问和修改 ref 的值。

reactive:用于对象类型

import { reactive } from 'vue';

// 创建 reactive 对象
const user = reactive({
name: '张三',
age: 25,
email: 'zhangsan@example.com'
});

// 修改对象属性
user.name = '李四';
user.age = 26;

对比总结

特性refreactive
用途基本类型对象和数组
访问值需要 .value直接访问
解包模板中自动解包不会解包
最佳实践混合使用对象使用 reactive

2.2 computed 计算属性

计算属性基于响应式数据自动计算,具有缓存特性:

import { ref, computed } from 'vue';

export default {
setup() {
const firstName = ref('张');
const lastName = ref('三');

// 只读计算属性
const fullName = computed(() => {
return firstName.value + lastName.value;
});

// 可写计算属性
const fullNameWritable = computed({
get() {
return firstName.value + lastName.value;
},
set(newName) {
const [first, last] = newName.split(' ');
firstName.value = first;
lastName.value = last;
}
});

return {
firstName,
lastName,
fullName,
fullNameWritable
};
}
};

2.3 侦听器

watch

import { ref, watch } from 'vue';

export default {
setup() {
const count = ref(0);
const count2 = ref(0);

// 监听单个 ref
watch(count, (newVal, oldVal) => {
console.log(`count 变化: ${oldVal} -> ${newVal}`);
});

// 监听多个 ref
watch([count, count2], ([newCount, newCount2]) => {
console.log(`两者都变化: ${newCount}, ${newCount2}`);
});

// 深度监听 reactive 对象
const user = reactive({ name: '张三', address: { city: '北京' } });

watch(user, (newUser) => {
console.log('user 对象变化:', newUser);
}, { deep: true });

return { count, count2, user };
}
};

watchEffect

import { ref, watchEffect } from 'vue';

export default {
setup() {
const count = ref(0);

// 自动依赖收集
watchEffect(() => {
console.log(`当前计数: ${count.value}`);
});

return { count };
}
};

watchEffect 会自动收集函数内使用的所有响应式数据,而 watch 需要手动指定要监听的数据。

2.4 生命周期钩子

Composition API 使用与 Options API 等效的生命周期钩子:

import {
onMounted,
onUpdated,
onUnmounted,
onBeforeMount,
onBeforeUpdate,
onBeforeUnmount
} from 'vue';

export default {
setup() {
onBeforeMount(() => {
console.log('组件挂载前');
});

onMounted(() => {
console.log('组件已挂载');
// 可以在这里初始化 API、添加事件监听器等
});

onBeforeUpdate(() => {
console.log('组件更新前');
});

onUpdated(() => {
console.log('组件已更新');
// 需要操作 DOM 时使用
});

onBeforeUnmount(() => {
console.log('组件卸载前');
// 清理副作用
});

onUnmounted(() => {
console.log('组件已卸载');
});

return {};
}
};

三、组合式函数(Composables)

3.1 什么是组合式函数

组合式函数是 Vue 3 中复用逻辑的核心模式。类似于 Vue 2 的 mixins,但更加灵活和可控。

3.2 创建自定义组合式函数

// composable/useCounter.js
import { ref, computed, watchEffect } from 'vue';

export function useCounter(initialValue = 0) {
const count = ref(initialValue);

// 方法
const increment = () => {
count.value++;
};

const decrement = () => {
count.value--;
};

const reset = () => {
count.value = initialValue;
};

// 计算属性
const isPositive = computed(() => count.value > 0);
const isNegative = computed(() => count.value < 0);
const isZero = computed(() => count.value === 0);

// 监听器
watchEffect(() => {
console.log(`计数器变化: ${count.value}`);
});

return {
count,
increment,
decrement,
reset,
isPositive,
isNegative,
isZero
};
}

3.3 使用组合式函数

// App.vue
import { useCounter } from './composable/useCounter';

export default {
setup() {
const {
count,
increment,
decrement,
reset,
isPositive,
isNegative,
isZero
} = useCounter(0);

return {
count,
increment,
decrement,
reset,
isPositive,
isNegative,
isZero
};
}
};
<template>
<div>
<h2>计数器</h2>
<p>当前计数: {{ count }}</p>
<p>是否为正数: {{ isPositive }}</p>
<p>是否为负数: {{ isNegative }}</p>
<p>是否为零: {{ isZero }}</p>

<button @click="increment">增加</button>
<button @click="decrement">减少</button>
<button @click="reset">重置</button>
</div>
</template>

3.4 实用组合式函数示例

useLocalStorage

// composable/useLocalStorage.js
import { ref, watch } from 'vue';

export function useLocalStorage(key, initialValue) {
// 从 localStorage 初始化
const storedValue = localStorage.getItem(key) || initialValue;
const value = ref(storedValue);

// 监听变化并保存到 localStorage
watch(value, (newValue) => {
localStorage.setItem(key, JSON.stringify(newValue));
}, { deep: true });

return value;
}

useMousePosition

// composable/useMousePosition.js
import { ref, onMounted, onUnmounted } from 'vue';

export function useMousePosition() {
const x = ref(0);
const y = ref(0);

const updatePosition = (event) => {
x.value = event.clientX;
y.value = event.clientY;
};

onMounted(() => {
window.addEventListener('mousemove', updatePosition);
});

onUnmounted(() => {
window.removeEventListener('mousemove', updatePosition);
});

return { x, y };
}

useFetch

// composable/useFetch.js
import { ref, onMounted } from 'vue';

export function useFetch(url) {
const data = ref(null);
const loading = ref(true);
const error = ref(null);

const fetchData = async () => {
try {
loading.value = true;
const response = await fetch(url);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
data.value = await response.json();
} catch (err) {
error.value = err.message;
} finally {
loading.value = false;
}
};

onMounted(() => {
fetchData();
});

return { data, loading, error };
}

四、进阶技巧

4.1 依赖注入

使用 provideinject 实现跨层级组件通信:

// 父组件
import { provide } from 'vue';

export default {
setup() {
const theme = ref('dark');
const userName = ref('张三');

// 提供数据和方法
provide('theme', theme);
provide('userName', userName);

// 提供方法
provide('updateTheme', (newTheme) => {
theme.value = newTheme;
});

return { theme };
}
};
// 子组件
import { inject } from 'vue';

export default {
setup() {
const theme = inject('theme', 'light'); // 默认值
const userName = inject('userName');
const updateTheme = inject('updateTheme');

const changeTheme = () => {
updateTheme('light');
};

return { theme, userName, changeTheme };
}
};

4.2 toRefs 和 toRef

从 reactive 对象中解构出来的属性会失去响应性,使用 toRefs 可以保持响应性:

import { reactive, toRefs } from 'vue';

export default {
setup() {
const state = reactive({
count: 0,
name: '张三',
age: 25
});

// 解构响应式对象
const { count, name, age } = toRefs(state);

const increment = () => {
state.count++;
};

return { count, name, age, increment };
}
};

4.3 shallowRef 和 shallowReactive

浅层响应式,只追踪顶层的属性变化:

import { shallowRef, shallowReactive } from 'vue';

// shallowRef
const state = shallowRef({
count: 0,
user: {
name: '张三'
}
});

// 只能修改 count
state.value.count = 1; // 触发更新
// state.value.user.name = '李四'; // 不会触发更新

// shallowReactive
const user = shallowReactive({
name: '张三',
address: {
city: '北京'
}
});

// 只能修改顶层属性
user.name = '李四'; // 触发更新
// user.address.city = '上海'; // 不会触发更新

五、实战案例

5.1 主题切换组件

// components/ThemeToggle.vue
<script>
import { ref, watch } from 'vue';

export default {
name: 'ThemeToggle',
setup() {
const isDark = ref(false);

// 从 localStorage 加载主题
const savedTheme = localStorage.getItem('theme');
if (savedTheme === 'dark') {
isDark.value = true;
}

// 监听主题变化
watch(isDark, (newValue) => {
const root = document.documentElement;
const theme = newValue ? 'dark' : 'light';

root.setAttribute('data-theme', theme);
localStorage.setItem('theme', theme);

// 切换背景色
root.style.backgroundColor = newValue ? '#1a1a1a' : '#ffffff';
root.style.color = newValue ? '#e0e0e0' : '#333333';
}, { immediate: true });

return { isDark };
}
};
</script>

<template>
<button @click="isDark = !isDark">
切换到 {{ isDark ? '浅色' : '深色' }} 主题
</button>
</template>

<style scoped>
button {
padding: 10px 20px;
border: none;
border-radius: 5px;
background: #4f46e5;
color: white;
cursor: pointer;
transition: all 0.3s;
}

button:hover {
background: #4338ca;
transform: translateY(-2px);
}

button:active {
transform: translateY(0);
}
</style>

5.2 表单验证组件

// components/UserForm.vue
<script>
import { ref } from 'vue';

export default {
name: 'UserForm',
emits: ['submit'],
setup(props, { emit }) {
const formData = ref({
username: '',
email: '',
age: '',
password: ''
});

const errors = ref({});
const isSubmitting = ref(false);

const validateForm = () => {
const newErrors = {};

// 验证用户名
if (!formData.value.username) {
newErrors.username = '用户名不能为空';
} else if (formData.value.username.length < 3) {
newErrors.username = '用户名至少 3 个字符';
}

// 验证邮箱
if (!formData.value.email) {
newErrors.email = '邮箱不能为空';
} else if (!/^\S+@\S+\.\S+$/.test(formData.value.email)) {
newErrors.email = '邮箱格式不正确';
}

// 验证年龄
if (!formData.value.age) {
newErrors.age = '年龄不能为空';
} else if (formData.value.age < 18 || formData.value.age > 100) {
newErrors.age = '年龄必须在 18-100 之间';
}

// 验证密码
if (!formData.value.password) {
newErrors.password = '密码不能为空';
} else if (formData.value.password.length < 6) {
newErrors.password = '密码至少 6 个字符';
}

errors.value = newErrors;
return Object.keys(newErrors).length === 0;
};

const handleSubmit = async () => {
if (!validateForm()) {
return;
}

isSubmitting.value = true;

try {
// 模拟 API 请求
await new Promise(resolve => setTimeout(resolve, 1000));
emit('submit', formData.value);
} finally {
isSubmitting.value = false;
}
};

const handleChange = (field, value) => {
formData.value[field] = value;
// 清除该字段的错误
if (errors.value[field]) {
delete errors.value[field];
}
};

return {
formData,
errors,
isSubmitting,
validateForm,
handleSubmit,
handleChange
};
}
};
</script>

<template>
<div class="user-form">
<form @submit.prevent="handleSubmit">
<div class="form-group">
<label for="username">用户名</label>
<input
id="username"
v-model="formData.username"
@input="handleChange('username', $event.target.value)"
type="text"
:class="{ 'is-invalid': errors.username }"
/>
<div v-if="errors.username" class="error">{{ errors.username }}</div>
</div>

<div class="form-group">
<label for="email">邮箱</label>
<input
id="email"
v-model="formData.email"
@input="handleChange('email', $event.target.value)"
type="email"
:class="{ 'is-invalid': errors.email }"
/>
<div v-if="errors.email" class="error">{{ errors.email }}</div>
</div>

<div class="form-group">
<label for="age">年龄</label>
<input
id="age"
v-model="formData.age"
@input="handleChange('age', $event.target.value)"
type="number"
min="18"
max="100"
:class="{ 'is-invalid': errors.age }"
/>
<div v-if="errors.age" class="error">{{ errors.age }}</div>
</div>

<div class="form-group">
<label for="password">密码</label>
<input
id="password"
v-model="formData.password"
@input="handleChange('password', $event.target.value)"
type="password"
:class="{ 'is-invalid': errors.password }"
/>
<div v-if="errors.password" class="error">{{ errors.password }}</div>
</div>

<button type="submit" :disabled="isSubmitting">
{{ isSubmitting ? '提交中...' : '提交' }}
</button>
</form>
</div>
</template>

<style scoped>
.user-form {
max-width: 500px;
margin: 0 auto;
padding: 20px;
background: white;
border-radius: 10px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
}

.form-group {
margin-bottom: 20px;
}

.form-group label {
display: block;
margin-bottom: 8px;
font-weight: 600;
color: #333;
}

.form-group input {
width: 100%;
padding: 10px;
border: 1px solid #ddd;
border-radius: 5px;
font-size: 16px;
transition: border-color 0.3s;
}

.form-group input:focus {
outline: none;
border-color: #4f46e5;
}

.form-group input.is-invalid {
border-color: #dc3545;
}

.error {
color: #dc3545;
font-size: 14px;
margin-top: 5px;
}

button {
width: 100%;
padding: 12px;
background: #4f46e5;
color: white;
border: none;
border-radius: 5px;
font-size: 16px;
cursor: pointer;
transition: background 0.3s;
}

button:hover:not(:disabled) {
background: #4338ca;
}

button:disabled {
background: #9ca3af;
cursor: not-allowed;
}
</style>

5.3 无限滚动列表

// components/InfiniteScroll.vue
<script>
import { ref, onMounted, onUnmounted } from 'vue';

export default {
name: 'InfiniteScroll',
props: {
fetchItems: {
type: Function,
required: true
},
pageSize: {
type: Number,
default: 10
},
hasMore: {
type: Boolean,
default: true
}
},
emits: ['loaded'],
setup(props, { emit }) {
const items = ref([]);
const loading = ref(false);
const currentPage = ref(1);
const observer = ref(null);

const loadMore = async () => {
if (loading.value || !props.hasMore) {
return;
}

loading.value = true;

try {
const newItems = await props.fetchItems(currentPage.value);
items.value = [...items.value, ...newItems];
currentPage.value++;

if (newItems.length === 0) {
// 没有更多数据了
}

emit('loaded', {
items: newItems,
page: currentPage.value,
hasMore: newItems.length > 0
});
} catch (error) {
console.error('加载失败:', error);
} finally {
loading.value = false;
}
};

const setupIntersectionObserver = () => {
if (typeof IntersectionObserver === 'undefined') {
return;
}

const observerOptions = {
root: null,
rootMargin: '0px',
threshold: 0.1
};

observer.value = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
loadMore();
}
});
}, observerOptions);
};

const cleanup = () => {
if (observer.value) {
observer.value.disconnect();
observer.value = null;
}
};

onMounted(() => {
loadMore();
setupIntersectionObserver();

// 找到触发器元素并添加 observer
const triggerElement = document.getElementById('infinite-scroll-trigger');
if (triggerElement && observer.value) {
observer.value.observe(triggerElement);
}
});

onUnmounted(() => {
cleanup();
});

return {
items,
loading,
loadMore
};
}
};
</script>

<template>
<div class="infinite-scroll">
<div v-for="item in items" :key="item.id" class="item">
{{ item.content }}
</div>

<div
id="infinite-scroll-trigger"
class="trigger"
v-if="loading || hasMore"
>
{{ loading ? '加载中...' : '没有更多了' }}
</div>
</div>
</template>

<style scoped>
.infinite-scroll {
max-width: 600px;
margin: 0 auto;
padding: 20px;
}

.item {
padding: 15px;
margin-bottom: 10px;
background: white;
border-radius: 5px;
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
transition: transform 0.3s;
}

.item:hover {
transform: translateX(5px);
}

.trigger {
padding: 20px;
text-align: center;
color: #666;
}
</style>

六、最佳实践

6.1 代码组织

setup() 函数中,按逻辑组织代码:

export default {
setup() {
// 1. 引入依赖
import { ref, computed, onMounted } from 'vue';

// 2. 声明响应式数据
const count = ref(0);

// 3. 声明计算属性
const doubleCount = computed(() => count.value * 2);

// 4. 声明方法
const increment = () => {
count.value++;
};

// 5. 声明侦听器
watch(count, (newVal) => {
console.log('计数变化:', newVal);
});

// 6. 生命周期钩子
onMounted(() => {
console.log('组件已挂载');
});

// 7. 返回给模板使用
return { count, doubleCount, increment };
}
};

6.2 函数命名

使用有意义的函数名和清晰的注释:

// 好的命名
const fetchUserData = async (userId) => { ... };
const validateEmail = (email) => { ... };
const formatDate = (date) => { ... };

// 不好的命名
const f = async (id) => { ... };
const v = (e) => { ... };
const d = (date) => { ... };

6.3 错误处理

const fetchData = async () => {
try {
const response = await fetch('/api/data');
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
return data;
} catch (error) {
console.error('获取数据失败:', error);
// 提供默认值或显示错误提示
return [];
}
};

6.4 性能优化

  1. 避免不必要的响应式

    // 只对需要响应式的数据使用 ref/reactive
    const data = { a: 1, b: 2, c: 3 };

    // 使用 computed 缓存计算结果
    const total = computed(() => {
    return data.a + data.b + data.c;
    });
  2. 合理使用 toRefs

    import { reactive, toRefs } from 'vue';

    const state = reactive({ count: 0, name: '张三' });

    // 好的做法
    const { count, name } = toRefs(state);

    // 不好的做法
    const count = ref(state.count); // 失去响应性
  3. 使用 useMemo 缓存计算结果

    import { ref, useMemo } from 'vue';

    const items = ref([...]);

    const filteredItems = useMemo(() => {
    return items.value.filter(item => item.active);
    }, [items]);

七、总结

Vue 3 Composition API 带来了许多强大的功能:

  1. 更好的代码组织:相关逻辑集中在一起
  2. 灵活的逻辑复用:通过组合式函数
  3. 更好的 TypeScript 支持:类型推断更准确
  4. 更简洁的语法:减少了样板代码

通过合理使用 Composition API,我们可以构建出更易维护、更易测试、更易扩展的应用程序。

希望本文能帮助你深入理解 Vue 3 Composition API,并在实际项目中灵活运用!