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 在简单场景下很好用,但随着项目规模扩大,会出现以下问题:
相关逻辑分散 :同一个功能的代码分散在 data、computed、methods、mounted 等不同的选项中代码复用困难 :抽取可复用逻辑需要使用 mixins,但可能导致命名冲突和逻辑不清晰TypeScript 支持不友好 :类型推断能力有限代码可读性下降 :大型组件中难以快速定位相关逻辑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 ref 和 reactive 是 Vue 3 中创建响应式数据的核心方法。
ref:用于基本类型 import { ref } from 'vue' ;const count = ref (0 );const message = ref ('Hello Vue 3' );const isActive = ref (false );console .log (count.value ); count.value = 1 ; console .log (count.value );
重要提示 :在模板中使用时,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' ;const user = reactive ({ name : '张三' , age : 25 , email : 'zhangsan@example.com' }); user.name = '李四' ; user.age = 26 ;
对比总结 :
特性 ref reactive 用途 基本类型 对象和数组 访问值 需要 .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 ); watch (count, (newVal, oldVal ) => { console .log (`count 变化: ${oldVal} -> ${newVal} ` ); }); watch ([count, count2], ([newCount, newCount2] ) => { console .log (`两者都变化: ${newCount} , ${newCount2} ` ); }); 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 ('组件已挂载' ); }); onBeforeUpdate (() => { console .log ('组件更新前' ); }); onUpdated (() => { console .log ('组件已更新' ); }); onBeforeUnmount (() => { console .log ('组件卸载前' ); }); onUnmounted (() => { console .log ('组件已卸载' ); }); return {}; } };
三、组合式函数(Composables) 3.1 什么是组合式函数 组合式函数是 Vue 3 中复用逻辑的核心模式。类似于 Vue 2 的 mixins,但更加灵活和可控。
3.2 创建自定义组合式函数 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 使用组合式函数 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 import { ref, watch } from 'vue' ;export function useLocalStorage (key, initialValue ) { const storedValue = localStorage .getItem (key) || initialValue; const value = ref (storedValue); watch (value, (newValue ) => { localStorage .setItem (key, JSON .stringify (newValue)); }, { deep : true }); return value; }
useMousePosition 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 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 依赖注入 使用 provide 和 inject 实现跨层级组件通信:
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' ;const state = shallowRef ({ count : 0 , user : { name : '张三' } }); state.value .count = 1 ; const user = shallowReactive ({ name : '张三' , address : { city : '北京' } }); user.name = '李四' ;
五、实战案例 5.1 主题切换组件 <script> import { ref, watch } from 'vue' ;export default { name : 'ThemeToggle' , setup ( ) { const isDark = ref (false ); 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 表单验证组件 <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 { 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 无限滚动列表 <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 (); 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 ( ) { import { ref, computed, onMounted } from 'vue' ; const count = ref (0 ); const doubleCount = computed (() => count.value * 2 ); const increment = ( ) => { count.value ++; }; watch (count, (newVal ) => { console .log ('计数变化:' , newVal); }); onMounted (() => { console .log ('组件已挂载' ); }); 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 性能优化 避免不必要的响应式
const data = { a : 1 , b : 2 , c : 3 };const total = computed (() => { return data.a + data.b + data.c ; });
合理使用 toRefs
import { reactive, toRefs } from 'vue' ;const state = reactive ({ count : 0 , name : '张三' });const { count, name } = toRefs (state);const count = ref (state.count );
使用 useMemo 缓存计算结果
import { ref, useMemo } from 'vue' ;const items = ref ([...]);const filteredItems = useMemo (() => { return items.value .filter (item => item.active ); }, [items]);
七、总结 Vue 3 Composition API 带来了许多强大的功能:
更好的代码组织 :相关逻辑集中在一起灵活的逻辑复用 :通过组合式函数更好的 TypeScript 支持 :类型推断更准确更简洁的语法 :减少了样板代码通过合理使用 Composition API,我们可以构建出更易维护、更易测试、更易扩展的应用程序。
希望本文能帮助你深入理解 Vue 3 Composition API,并在实际项目中灵活运用!