北屯市网站建设_网站建设公司_Node.js_seo优化
2025/12/18 20:04:44 网站建设 项目流程

Vue3的Composition API彻底改变了Vue的开发方式,本文将深入剖析组合式API的核心概念和最佳实践,帮助你快速掌握Vue3开发。

一、为什么需要Composition API?

1.1 Options API的痛点

痛点表现:

  • 逻辑分散:相关代码被data、methods、computed等选项分割
  • 代码复用困难:mixins容易命名冲突,来源不清晰
  • 类型推导弱:TypeScript支持不够友好
  • 大组件难维护:代码量大时难以理解和维护
// ❌ Options API:逻辑分散exportdefault{data(){return{count:0,user:null,loading:false}},computed:{doubleCount(){returnthis.count*2}},methods:{increment(){this.count++},asyncfetchUser(){this.loading=truethis.user=awaitapi.getUser()this.loading=false}},mounted(){this.fetchUser()}}

1.2 Composition API的优势

  • ✅ 逻辑聚合:相关代码组织在一起
  • ✅ 代码复用:通过组合函数轻松复用
  • ✅ 类型推导:完美支持TypeScript
  • ✅ Tree-shaking:未使用的代码可以被移除

二、核心API详解

2.1 ref 和 reactive

关键点:ref用于基本类型,reactive用于对象类型。

<template> <div> <!-- ref需要.value访问,模板中自动解包 --> <p>计数: {{ count }}</p> <p>双倍: {{ doubleCount }}</p> <!-- reactive对象直接访问属性 --> <p>用户: {{ user.name }}</p> <p>年龄: {{ user.age }}</p> <button @click="increment">增加</button> <button @click="updateUser">更新用户</button> </div> </template> <script setup> import { ref, reactive, computed } from 'vue' // ✅ ref:基本类型响应式 const count = ref(0) const message = ref('Hello') // 在JS中需要.value访问 console.log(count.value) // 0 count.value++ console.log(count.value) // 1 // ✅ reactive:对象类型响应式 const user = reactive({ name: '张三', age: 25, address: { city: '北京' } }) // 直接访问属性 console.log(user.name) // 张三 user.age = 26 // computed计算属性 const doubleCount = computed(() => count.value * 2) // 方法 const increment = () => { count.value++ } const updateUser = () => { user.name = '李四' user.age = 30 } // ❌ 常见错误:解构reactive会失去响应式 const { name, age } = user // 失去响应式! // ✅ 正确:使用toRefs保持响应式 import { toRefs } from 'vue' const { name, age } = toRefs(user) // 保持响应式 </script>

痛点解决:清晰的响应式数据管理,避免this指向问题。


2.2 生命周期钩子

关键点:组合式API中的生命周期钩子以on开头。

<script setup> import { onBeforeMount, onMounted, onBeforeUpdate, onUpdated, onBeforeUnmount, onUnmounted, onErrorCaptured } from 'vue' // 组件挂载前 onBeforeMount(() => { console.log('组件即将挂载') }) // 组件挂载后(最常用) onMounted(() => { console.log('组件已挂载') // 适合:DOM操作、发起请求、初始化第三方库 fetchData() initChart() }) // 组件更新前 onBeforeUpdate(() => { console.log('组件即将更新') }) // 组件更新后 onUpdated(() => { console.log('组件已更新') // 注意:避免在这里修改状态,可能导致无限循环 }) // 组件卸载前 onBeforeUnmount(() => { console.log('组件即将卸载') // 适合:清理定时器、取消请求、移除事件监听 }) // 组件卸载后 onUnmounted(() => { console.log('组件已卸载') }) // 错误捕获 onErrorCaptured((err, instance, info) => { console.error('捕获到错误:', err) return false // 阻止错误继续传播 }) // ✅ 可以多次调用同一个钩子 onMounted(() => { console.log('第一个mounted') }) onMounted(() => { console.log('第二个mounted') }) </script>

对比Options API:

Options APIComposition API
beforeCreatesetup()
createdsetup()
beforeMountonBeforeMount
mountedonMounted
beforeUpdateonBeforeUpdate
updatedonUpdated
beforeUnmountonBeforeUnmount
unmountedonUnmounted

2.3 watch 和 watchEffect

关键点:watch需要明确指定监听源,watchEffect自动追踪依赖。

<script setup> import { ref, reactive, watch, watchEffect } from 'vue' const count = ref(0) const user = reactive({ name: '张三', age: 25 }) // ✅ watch:监听单个ref watch(count, (newVal, oldVal) => { console.log(`count从${oldVal}变为${newVal}`) }) // ✅ watch:监听多个源 watch([count, () => user.age], ([newCount, newAge], [oldCount, oldAge]) => { console.log('count或age变化了') }) // ✅ watch:监听reactive对象(深度监听) watch(user, (newVal, oldVal) => { console.log('user对象变化了') // 注意:newVal和oldVal是同一个对象引用 }, { deep: true }) // ✅ watch:监听reactive对象的某个属性 watch(() => user.name, (newName, oldName) => { console.log(`name从${oldName}变为${newName}`) }) // ✅ watchEffect:自动追踪依赖 watchEffect(() => { // 自动追踪count和user.age的变化 console.log(`count: ${count.value}, age: ${user.age}`) }) // watch选项 watch(count, (newVal) => { console.log(newVal) }, { immediate: true, // 立即执行一次 deep: true, // 深度监听 flush: 'post' // 在DOM更新后执行 }) // 停止监听 const stop = watch(count, (newVal) => { console.log(newVal) if (newVal > 10) { stop() // 停止监听 } }) // ❌ 常见错误:监听reactive对象的属性 watch(user.name, (newVal) => { // 错误! console.log(newVal) }) // ✅ 正确写法 watch(() => user.name, (newVal) => { console.log(newVal) }) </script>

痛点解决:灵活的数据监听,避免不必要的性能开销。


2.4 computed 计算属性

关键点:computed具有缓存特性,只有依赖变化时才重新计算。

<script setup> import { ref, computed } from 'vue' const firstName = ref('张') const lastName = ref('三') // ✅ 只读计算属性 const fullName = computed(() => { console.log('计算fullName') // 只在依赖变化时执行 return `${firstName.value} ${lastName.value}` }) // ✅ 可写计算属性 const fullNameWritable = computed({ get() { return `${firstName.value} ${lastName.value}` }, set(value) { const names = value.split(' ') firstName.value = names[0] lastName.value = names[1] } }) // 使用 console.log(fullName.value) // 张 三 fullNameWritable.value = '李 四' // 触发set console.log(firstName.value) // 李 console.log(lastName.value) // 四 // ❌ 常见错误:在computed中修改依赖 const badComputed = computed(() => { firstName.value = '王' // 错误!不要在computed中修改依赖 return firstName.value }) // ✅ computed vs method const list = ref([1, 2, 3, 4, 5]) // computed:有缓存,依赖不变不重新计算 const filteredList = computed(() => { console.log('计算filteredList') return list.value.filter(n => n > 2) }) // method:无缓存,每次调用都执行 const getFilteredList = () => { console.log('执行getFilteredList') return list.value.filter(n => n > 2) } </script> <template> <div> <!-- computed:多次访问只计算一次 --> <p>{{ filteredList }}</p> <p>{{ filteredList }}</p> <!-- method:每次都执行 --> <p>{{ getFilteredList() }}</p> <p>{{ getFilteredList() }}</p> </div> </template>

痛点解决:自动缓存计算结果,避免重复计算,提升性能。


三、组合函数(Composables)

3.1 基础组合函数

关键点:将可复用的逻辑提取为组合函数,实现代码复用。

// composables/useCounter.jsimport{ref,computed}from'vue'exportfunctionuseCounter(initialValue=0){constcount=ref(initialValue)constdoubleCount=computed(()=>count.value*2)constincrement=()=>{count.value++}constdecrement=()=>{count.value--}constreset=()=>{count.value=initialValue}return{count,doubleCount,increment,decrement,reset}}
<!-- 使用组合函数 --> <template> <div> <p>计数: {{ count }}</p> <p>双倍: {{ doubleCount }}</p> <button @click="increment">+1</button> <button @click="decrement">-1</button> <button @click="reset">重置</button> </div> </template> <script setup> import { useCounter } from '@/composables/useCounter' // 可以创建多个独立的计数器 const { count, doubleCount, increment, decrement, reset } = useCounter(10) const counter2 = useCounter(0) </script>

3.2 实战:鼠标位置追踪

// composables/useMouse.jsimport{ref,onMounted,onUnmounted}from'vue'exportfunctionuseMouse(){constx=ref(0)consty=ref(0)constupdate=(event)=>{x.value=event.pageX y.value=event.pageY}onMounted(()=>{window.addEventListener('mousemove',update)})onUnmounted(()=>{window.removeEventListener('mousemove',update)})return{x,y}}
<template> <div> <p>鼠标位置: {{ x }}, {{ y }}</p> </div> </template> <script setup> import { useMouse } from '@/composables/useMouse' const { x, y } = useMouse() </script>

3.3 实战:异步数据获取

// composables/useFetch.jsimport{ref,watchEffect,toValue}from'vue'exportfunctionuseFetch(url){constdata=ref(null)consterror=ref(null)constloading=ref(false)constfetchData=async()=>{loading.value=trueerror.value=nulldata.value=nulltry{// toValue可以处理ref和普通值consturlValue=toValue(url)constresponse=awaitfetch(urlValue)if(!response.ok){thrownewError(`HTTP error! status:${response.status}`)}data.value=awaitresponse.json()}catch(e){error.value=e.message}finally{loading.value=false}}// 当url变化时重新获取watchEffect(()=>{fetchData()})constrefetch=()=>{fetchData()}return{data,error,loading,refetch}}
<template> <div> <div v-if="loading">加载中...</div> <div v-else-if="error">错误: {{ error }}</div> <div v-else-if="data"> <h3>{{ data.title }}</h3> <p>{{ data.body }}</p> </div> <button @click="refetch">刷新</button> </div> </template> <script setup> import { ref } from 'vue' import { useFetch } from '@/composables/useFetch' const userId = ref(1) const url = computed(() => `https://jsonplaceholder.typicode.com/posts/${userId.value}`) const { data, error, loading, refetch } = useFetch(url) </script>

3.4 实战:本地存储

// composables/useLocalStorage.jsimport{ref,watch}from'vue'exportfunctionuseLocalStorage(key,defaultValue){// 从localStorage读取初始值conststoredValue=localStorage.getItem(key)constdata=ref(storedValue?JSON.parse(storedValue):defaultValue)// 监听变化并同步到localStoragewatch(data,(newValue)=>{localStorage.setItem(key,JSON.stringify(newValue))},{deep:true})// 清除constremove=()=>{localStorage.removeItem(key)data.value=defaultValue}return{data,remove}}
<template> <div> <input v-model="username.data" placeholder="输入用户名"> <p>保存的用户名: {{ username.data }}</p> <button @click="username.remove">清除</button> </div> </template> <script setup> import { useLocalStorage } from '@/composables/useLocalStorage' const username = useLocalStorage('username', '') </script>

痛点解决:逻辑复用变得简单,代码更加模块化和可维护。


四、高级技巧

4.1 provide / inject 依赖注入

关键点:跨层级组件通信,避免props层层传递。

<!-- 父组件 --> <template> <div> <ChildComponent /> </div> </template> <script setup> import { provide, ref, readonly } from 'vue' import ChildComponent from './ChildComponent.vue' const theme = ref('dark') const user = ref({ name: '张三', role: 'admin' }) // 提供数据 provide('theme', theme) provide('user', readonly(user)) // 只读,防止子组件修改 // 提供方法 const updateTheme = (newTheme) => { theme.value = newTheme } provide('updateTheme', updateTheme) </script>
<!-- 子组件(任意层级) --> <template> <div :class="theme"> <p>当前主题: {{ theme }}</p> <p>用户: {{ user.name }}</p> <button @click="updateTheme('light')">切换主题</button> </div> </template> <script setup> import { inject } from 'vue' // 注入数据 const theme = inject('theme') const user = inject('user') const updateTheme = inject('updateTheme') // 提供默认值 const config = inject('config', { timeout: 3000 }) </script>

4.2 defineExpose 暴露组件方法

关键点:<script setup>默认是封闭的,需要显式暴露给父组件。

<!-- 子组件 --> <template> <div> <p>{{ message }}</p> </div> </template> <script setup> import { ref } from 'vue' const message = ref('Hello') const count = ref(0) const updateMessage = (newMessage) => { message.value = newMessage } const increment = () => { count.value++ } // 暴露给父组件 defineExpose({ message, count, updateMessage, increment }) </script>
<!-- 父组件 --> <template> <div> <ChildComponent ref="childRef" /> <button @click="callChildMethod">调用子组件方法</button> </div> </template> <script setup> import { ref } from 'vue' import ChildComponent from './ChildComponent.vue' const childRef = ref(null) const callChildMethod = () => { // 访问子组件暴露的属性和方法 console.log(childRef.value.message) childRef.value.updateMessage('New Message') childRef.value.increment() } </script>

4.3 自定义指令

// directives/vFocus.jsexportconstvFocus={mounted(el){el.focus()}}// directives/vClickOutside.jsexportconstvClickOutside={mounted(el,binding){el.clickOutsideEvent=(event)=>{if(!(el===event.target||el.contains(event.target))){binding.value(event)}}document.addEventListener('click',el.clickOutsideEvent)},unmounted(el){document.removeEventListener('click',el.clickOutsideEvent)}}
<template> <div> <!-- 自动聚焦 --> <input v-focus placeholder="自动聚焦"> <!-- 点击外部关闭 --> <div v-click-outside="closeDropdown" class="dropdown"> <button @click="showDropdown = !showDropdown">下拉菜单</button> <ul v-if="showDropdown"> <li>选项1</li> <li>选项2</li> </ul> </div> </div> </template> <script setup> import { ref } from 'vue' import { vFocus, vClickOutside } from '@/directives' const showDropdown = ref(false) const closeDropdown = () => { showDropdown.value = false } </script>

五、实战案例:Todo应用

<template> <div class="todo-app"> <h1>待办事项</h1> <!-- 输入框 --> <div class="input-box"> <input v-model="newTodo" @keyup.enter="addTodo" placeholder="添加新任务..." > <button @click="addTodo">添加</button> </div> <!-- 过滤器 --> <div class="filters"> <button v-for="filter in filters" :key="filter" :class="{ active: currentFilter === filter }" @click="currentFilter = filter" > {{ filter }} </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="removeTodo(todo.id)">删除</button> </li> </ul> <!-- 统计 --> <div class="stats"> <span>总计: {{ todos.length }}</span> <span>已完成: {{ completedCount }}</span> <span>未完成: {{ activeCount }}</span> </div> </div> </template> <script setup> import { ref, computed } from 'vue' import { useLocalStorage } from '@/composables/useLocalStorage' // 使用本地存储 const { data: todos } = useLocalStorage('todos', []) const newTodo = ref('') const currentFilter = ref('全部') const filters = ['全部', '未完成', '已完成'] // 添加任务 const addTodo = () => { if (!newTodo.value.trim()) return todos.value.push({ id: Date.now(), text: newTodo.value, completed: false }) newTodo.value = '' } // 删除任务 const removeTodo = (id) => { const index = todos.value.findIndex(todo => todo.id === id) if (index > -1) { todos.value.splice(index, 1) } } // 过滤任务 const filteredTodos = computed(() => { switch (currentFilter.value) { case '未完成': return todos.value.filter(todo => !todo.completed) case '已完成': return todos.value.filter(todo => todo.completed) default: return todos.value } }) // 统计 const completedCount = computed(() => todos.value.filter(todo => todo.completed).length ) const activeCount = computed(() => todos.value.filter(todo => !todo.completed).length ) </script> <style scoped> .todo-app { max-width: 600px; margin: 50px auto; padding: 20px; } .input-box { display: flex; gap: 10px; margin-bottom: 20px; } .input-box input { flex: 1; padding: 10px; border: 1px solid #ddd; border-radius: 4px; } .filters { display: flex; gap: 10px; margin-bottom: 20px; } .filters button { padding: 8px 16px; border: 1px solid #ddd; background: white; cursor: pointer; border-radius: 4px; } .filters button.active { background: #42b983; color: white; border-color: #42b983; } .todo-list { list-style: none; padding: 0; } .todo-list li { display: flex; align-items: center; gap: 10px; padding: 10px; border-bottom: 1px solid #eee; } .todo-list li.completed span { text-decoration: line-through; color: #999; } .stats { display: flex; justify-content: space-around; margin-top: 20px; padding: 10px; background: #f5f5f5; border-radius: 4px; } </style>

六、最佳实践

✅ 命名规范

  • 组合函数以use开头:useCounteruseMouse
  • 事件处理函数以handleon开头:handleClickonSubmit
  • 布尔值以ishasshould开头:isLoadinghasError

✅ 代码组织

// 推荐的代码组织顺序<script setup>// 1. 导入import{ref,computed,watch}from'vue'import{useRouter}from'vue-router'// 2. 组合函数constrouter=useRouter()const{data,loading}=useFetch('/api/data')// 3. 响应式数据constcount=ref(0)constuser=reactive({name:''})// 4. 计算属性constdoubleCount=computed(()=>count.value*2)// 5. 监听器watch(count,(newVal)=>{console.log(newVal)})// 6. 生命周期onMounted(()=>{fetchData()})// 7. 方法constincrement=()=>{count.value++}// 8. 暴露defineExpose({increment})</script>

✅ 性能优化

  • 使用shallowRefshallowReactive处理大对象
  • 使用markRaw标记不需要响应式的对象
  • 合理使用computed缓存计算结果
  • 避免在模板中使用复杂表达式

七、总结

Vue3 Composition API的核心优势:

  1. 逻辑复用- 通过组合函数轻松复用逻辑
  2. 代码组织- 相关代码聚合在一起,易于维护
  3. 类型推导- 完美支持TypeScript
  4. 性能优化- 更好的Tree-shaking支持

记住:Composition API不是替代Options API,而是提供了更灵活的选择


相关资源

  • Vue3官方文档
  • VueUse - 强大的组合函数库
  • Pinia - Vue3状态管理

💡小贴士:从小项目开始尝试Composition API,逐步掌握其精髓!

关注我,获取更多Vue干货!🚀

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询