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
ref和reactive是Vue3响应式系统的核心。
ref
用于创建基础类型的响应式数据:
import { ref } from 'vue';
const count = ref(0); const name = ref('张三');
count.value++; name.value = '李四';
|
reactive
用于创建对象的响应式数据:
import { reactive } from 'vue';
const user = reactive({ name: '张三', age: 25, address: { city: '北京' } });
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}`; });
const fullNameWithSetter = computed({ get() { return `${firstName.value} ${lastName.value}`; }, set(newValue) { const [first, last] = newValue.split(' '); firstName.value = first; lastName.value = last; } });
|
watch 和 watchEffect
watch和watchEffect用于监听响应式数据的变化。
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}`); });
|
生命周期钩子
组合式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(); const ui = useUI(); return { ...user, ...data, ...ui }; } }
|
2. 性能优化
const largeData = shallowRef(complexObject);
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;
const { count } = toRefs(state);
|
2. 循环依赖问题
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确实让我受益匪浅:
- 更好的代码组织:相关逻辑放在一起,代码更易维护
- 强大的复用能力:自定义组合函数让逻辑复用变得简单
- 更好的TypeScript支持:类型定义更清晰
- 更好的性能:细粒度的响应式控制
当然,组合式API也有一些学习成本,比如响应式原理的理解、组合函数的设计等。但总的来说,它的优势是非常明显的。
最后给大家一个小建议:从小的组件开始尝试使用组合式API,逐步掌握各种特性的使用。遇到问题时,多看官方文档,多实践,相信你很快就能上手Vue3。
记住,技术只是工具,关键是用它来创造价值。希望这篇文章能对你有所帮助,让我们一起在Vue的世界里探索更精彩的未来!