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

Vue3组合式API完全手册

说实话,从Vue2升级到Vue3的时候,我一开始还挺犹豫的。组合式API(Composition API)这东西看着确实挺酷,但又要学习新概念,又要重构旧代码…用了一段时间后,我发现这玩意儿真香!今天就和大家分享一下Vue3组合式API的那些事儿。

什么是组合式API?

组合式API(Composition API)是Vue3引入的新特性,它让我们能够用JavaScript函数的方式组织和复用组件逻辑。相比于Vue2的Options API,组合式API提供了更好的逻辑组织和代码复用能力。

Options API vs Composition API

Vue2 Options API:

export default {
data() {
return {
count: 0,
message: 'Hello Vue2'
}
},
computed: {
doubleCount() {
return this.count * 2;
}
},
methods: {
increment() {
this.count++;
}
},
created() {
console.log('组件创建');
}
}

Vue3 Composition API:

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

export default {
setup() {
const count = ref(0);
const message = ref('Hello Vue3');

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

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

onMounted(() => {
console.log('组件创建');
});

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

核心概念详解

ref 和 reactive

refreactive是Vue3响应式系统的核心。

ref

用于创建基础类型的响应式数据:

import { ref } from 'vue';

const count = ref(0);
const name = ref('张三');

// 访问需要.value
count.value++; // 1
name.value = '李四'; // 更新值

reactive

用于创建对象的响应式数据:

import { reactive } from 'vue';

const user = reactive({
name: '张三',
age: 25,
address: {
city: '北京'
}
});

// 直接访问,不需要.value
user.age = 26;
user.address.city = '上海';

computed

计算属性,根据响应式数据自动更新:

import { ref, computed } from 'vue';

const firstName = ref('张');
const lastName = ref('三');

const fullName = computed(() => {
return `${firstName.value} ${lastName.value}`;
});

// 也可以设置getter和setter
const fullNameWithSetter = computed({
get() {
return `${firstName.value} ${lastName.value}`;
},
set(newValue) {
const [first, last] = newValue.split(' ');
firstName.value = first;
lastName.value = last;
}
});

watch 和 watchEffect

watchwatchEffect用于监听响应式数据的变化。

watch

精确监听特定数据的变化:

import { ref, watch } from 'vue';

const count = ref(0);
const name = ref('张三');

// 监听单个数据
watch(count, (newVal, oldVal) => {
console.log(`count从${oldVal}变成了${newVal}`);
});

// 监听多个数据
watch([count, name], ([newCount, newName], [oldCount, oldName]) => {
console.log(`count从${oldCount}变成${newCount},name从${oldName}变成${newName}`);
});

// 监听对象的特定属性
watch(() => user.age, (newAge, oldAge) => {
console.log(`年龄从${oldAge}变成${newAge}`);
}, { immediate: true }); // 立即执行一次

watchEffect

自动收集依赖,不需要手动指定监听的数据:

import { ref, watchEffect } from 'vue';

const count = ref(0);
const name = ref('张三');

watchEffect(() => {
console.log(`${name.value}的年龄是${count.value}`);
});

// 当count或name变化时,上面的函数会自动执行

生命周期钩子

组合式API提供了对应的生命周期钩子:

import { onMounted, onUpdated, onUnmounted } from 'vue';

export default {
setup() {
onMounted(() => {
console.log('组件挂载完成');
});

onUpdated(() => {
console.log('组件更新完成');
});

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

实用组合函数

组合式API的强大之处在于我们可以创建可复用的组合函数。

useMouse - 鼠标位置

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

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

const updatePosition = (e) => {
x.value = e.pageX;
y.value = e.pageY;
};

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

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

return { x, y };
}

使用方式:

import { useMouse } from './composables/useMouse';

export default {
setup() {
const { x, y } = useMouse();

return { x, y };
}
}

useFetch - 数据请求

import { ref, onMounted } from 'vue';

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

const fetchData = async () => {
loading.value = true;
error.value = null;

try {
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;
} finally {
loading.value = false;
}
};

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

return { data, loading, error, refetch: fetchData };
}

使用方式:

import { useFetch } from './composables/useFetch';

export default {
setup() {
const { data, loading, error, refetch } = useFetch('https://api.example.com/posts');

return { data, loading, error, refetch };
}
}

useLocalStorage - 本地存储

import { ref, watch } from 'vue';

export function useLocalStorage(key, initialValue) {
const storedValue = localStorage.getItem(key);
const value = ref(storedValue ? JSON.parse(storedValue) : initialValue);

watch(value, (newValue) => {
localStorage.setItem(key, JSON.stringify(newValue));
}, { deep: true });

return value;
}

实战案例:待办事项应用

完整实现

<template>
<div class="todo-app">
<h1>待办事项</h1>

<!-- 添加输入框 -->
<div class="input-section">
<input
v-model="newTodo"
@keyup.enter="addTodo"
placeholder="添加新的待办事项"
/>
<button @click="addTodo">添加</button>
</div>

<!-- 过滤器 -->
<div class="filter-section">
<button
v-for="filter in filters"
:key="filter.value"
:class="{ active: currentFilter === filter.value }"
@click="currentFilter = filter.value"
>
{{ filter.label }} ({{ filter.count }})
</button>
</div>

<!-- 待办事项列表 -->
<ul class="todo-list">
<li
v-for="todo in filteredTodos"
:key="todo.id"
:class="{ completed: todo.completed }"
>
<input
type="checkbox"
v-model="todo.completed"
/>
<span>{{ todo.text }}</span>
<button @click="deleteTodo(todo.id)">删除</button>
</li>
</ul>

<!-- 统计信息 -->
<div class="stats">
<p>总计: {{ stats.total }}</p>
<p>已完成: {{ stats.completed }}</p>
<p>进行中: {{ stats.active }}</p>
</div>
</div>
</template>

<script>
import { ref, computed, onMounted, watch } from 'vue';

export default {
setup() {
// 状态管理
const todos = ref([]);
const newTodo = ref('');
const currentFilter = ref('all');

// 过滤选项
const filters = [
{ value: 'all', label: '全部', count: () => todos.value.length },
{ value: 'active', label: '进行中', count: () => todos.value.filter(todo => !todo.completed).length },
{ value: 'completed', label: '已完成', count: () => todos.value.filter(todo => todo.completed).length }
];

// 计算属性
const filteredTodos = computed(() => {
switch (currentFilter.value) {
case 'active':
return todos.value.filter(todo => !todo.completed);
case 'completed':
return todos.value.filter(todo => todo.completed);
default:
return todos.value;
}
});

const stats = computed(() => {
const total = todos.value.length;
const completed = todos.value.filter(todo => todo.completed).length;
const active = total - completed;

return { total, completed, active };
});

// 方法
const addTodo = () => {
if (newTodo.value.trim()) {
todos.value.push({
id: Date.now(),
text: newTodo.value.trim(),
completed: false,
createdAt: new Date().toISOString()
});
newTodo.value = '';
}
};

const deleteTodo = (id) => {
const index = todos.value.findIndex(todo => todo.id === id);
if (index > -1) {
todos.value.splice(index, 1);
}
};

// 本地存储
const saveToStorage = () => {
localStorage.setItem('todos', JSON.stringify(todos.value));
};

const loadFromStorage = () => {
const saved = localStorage.getItem('todos');
if (saved) {
todos.value = JSON.parse(saved);
}
};

// 监听数据变化
watch(todos, saveToStorage, { deep: true });

// 组件挂载时加载数据
onMounted(() => {
loadFromStorage();
});

return {
todos,
newTodo,
currentFilter,
filters,
filteredTodos,
stats,
addTodo,
deleteTodo
};
}
};
</script>

<style scoped>
.todo-app {
max-width: 600px;
margin: 0 auto;
padding: 20px;
font-family: Arial, sans-serif;
}

.input-section {
display: flex;
margin-bottom: 20px;
}

.input-section input {
flex: 1;
padding: 8px;
margin-right: 10px;
border: 1px solid #ddd;
border-radius: 4px;
}

.input-section button {
padding: 8px 16px;
background: #42b983;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}

.filter-section {
margin-bottom: 20px;
}

.filter-section button {
margin-right: 10px;
padding: 6px 12px;
background: #f0f0f0;
border: none;
border-radius: 4px;
cursor: pointer;
}

.filter-section button.active {
background: #42b983;
color: white;
}

.todo-list {
list-style: none;
padding: 0;
}

.todo-list li {
display: flex;
align-items: center;
padding: 10px;
border-bottom: 1px solid #eee;
}

.todo-list li.completed span {
text-decoration: line-through;
color: #999;
}

.todo-list li input[type="checkbox"] {
margin-right: 10px;
}

.todo-list li button {
margin-left: auto;
padding: 4px 8px;
background: #ff4444;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}

.stats {
margin-top: 20px;
padding: 15px;
background: #f9f9f9;
border-radius: 4px;
}

.stats p {
margin: 5px 0;
}
</style>

高级特性

setup糖语法

Vue3提供了<script setup>语法糖,让代码更简洁:

<script setup>
import { ref, computed } from 'vue';

const count = ref(0);
const double = computed(() => count.value * 2);

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

<template>
<div>
<p>Count: {{ count }}</p>
<p>Double: {{ double }}</p>
<button @click="increment">+</button>
</div>
</template>

组件间通信

Props和Emit

<!-- Parent.vue -->
<script setup>
import Child from './Child.vue';

const message = 'Hello from parent';
const receiveMessage = (msg) => {
console.log('Received:', msg);
};
</script>

<template>
<Child :message="message" @update="receiveMessage" />
</template>
<!-- Child.vue -->
<script setup>
const props = defineProps(['message']);
const emit = defineEmits(['update']);

const sendMessage = () => {
emit('update', 'Hello from child');
};
</script>

<template>
<div>
<p>{{ props.message }}</p>
<button @click="sendMessage">发送消息</button>
</div>
</template>

provide和inject

<!-- Parent.vue -->
<script setup>
import { ref, provide } from 'vue';

const count = ref(0);
provide('count', count);
</script>

<template>
<Child />
</template>
<!-- Child.vue -->
<script setup>
import { inject } from 'vue';

const count = inject('count');
</script>

<template>
<div>Count: {{ count }}</div>
</template>

TypeScript支持

Vue3对TypeScript有很好的支持:

<script setup lang="ts">
import { ref, computed, defineProps, defineEmits } from 'vue';

// 定义props类型
interface Props {
message: string;
count?: number;
}

const props = defineProps<Props>();

// 定义emit类型
const emit = defineEmits<{
(e: 'update', value: string): void;
(e: 'delete', id: number): void;
}>();

// 使用ref
const localCount = ref(0);

// 使用computed
const doubleCount = computed(() => (props.count || 0) * 2);
</script>

最佳实践

1. 代码组织

// 按功能组织,而不是按选项组织
export default {
setup() {
// 用户相关逻辑
const user = useUser();

// 数据相关逻辑
const data = useData();

// UI相关逻辑
const ui = useUI();

return { ...user, ...data, ...ui };
}
}

2. 性能优化

// 使用shallowRef避免深层响应式
const largeData = shallowRef(complexObject);

// 使用computed缓存计算结果
const expensiveValue = computed(() => {
return heavyCalculation(data.value);
});

3. 错误处理

import { onErrorCaptured } from 'vue';

export default {
setup() {
const error = ref(null);

onErrorCaptured((err, instance, info) => {
error.value = err;
return false; // 阻止错误继续传播
});

return { error };
}
}

4. 测试

组合式API让测试变得更简单:

// 组件测试
import { mount } from '@vue/test-utils';
import MyComponent from './MyComponent.vue';

test('renders correctly', () => {
const wrapper = mount(MyComponent);
expect(wrapper.text()).toContain('Hello');
});

常见问题和解决方案

1. 响应式数据丢失

// 错误:解构会失去响应性
const { count } = state;
// count不再响应式

// 正确:使用toRefs
const { count } = toRefs(state);
// count保持响应式

2. 循环依赖问题

// 避免在computed中访问其他computed
const a = computed(() => b.value + 1);
const b = computed(() => a.value + 1); // 循环依赖

// 解决方案:拆分逻辑
const baseValue = ref(0);
const a = computed(() => baseValue.value + 1);
const b = computed(() => baseValue.value + 2);

3. 内存泄漏

// 确保清理副作用
import { onUnmounted } from 'vue';

export default {
setup() {
const timer = setInterval(() => {
// 定时任务
}, 1000);

onUnmounted(() => {
clearInterval(timer);
});

return {};
}
}

总结

Vue3组合式API真的彻底改变了我写Vue的方式。从逻辑复用到代码组织,从性能优化到TypeScript支持,组合式API带来了太多便利。

在我的开发经历中,组合式API确实让我受益匪浅:

  1. 更好的代码组织:相关逻辑放在一起,代码更易维护
  2. 强大的复用能力:自定义组合函数让逻辑复用变得简单
  3. 更好的TypeScript支持:类型定义更清晰
  4. 更好的性能:细粒度的响应式控制

当然,组合式API也有一些学习成本,比如响应式原理的理解、组合函数的设计等。但总的来说,它的优势是非常明显的。

最后给大家一个小建议:从小的组件开始尝试使用组合式API,逐步掌握各种特性的使用。遇到问题时,多看官方文档,多实践,相信你很快就能上手Vue3。

记住,技术只是工具,关键是用它来创造价值。希望这篇文章能对你有所帮助,让我们一起在Vue的世界里探索更精彩的未来!