Vue3 Composition API通过组合式函数(Composables)实现组件逻辑复用,替代了Options API的mixins方案。响应式系统基于Proxy实现,提供了ref、reactive、computed、watch等核心API。前端开发中,合理封装组合式函数可将复杂业务逻辑从组件中抽离,提升代码可维护性和复用率。本文从Vue3生态的响应式原理出发,结合前端工程化实践,拆解组合式函数的设计规范和典型应用场景。
Composition API核心响应式原理
Vue3响应式系统基于ES6 Proxy实现。reactive()函数将对象包装为Proxy,拦截get/set操作并建立依赖追踪:
// Vue3响应式系统简化实现
const targetMap = new WeakMap() // 存储依赖关系
let activeEffect = null
function effect(fn) {
activeEffect = fn
fn()
activeEffect = null
}
function track(target, key) {
if (!activeEffect) return
let depsMap = targetMap.get(target)
if (!depsMap) {
depsMap = new Map()
targetMap.set(target, depsMap)
}
let dep = depsMap.get(key)
if (!dep) {
dep = new Set()
depsMap.set(key, dep)
}
dep.add(activeEffect)
}
function trigger(target, key) {
const depsMap = targetMap.get(target)
if (!depsMap) return
const dep = depsMap.get(key)
if (dep) {
dep.forEach(effect => effect())
}
}
function reactive(target) {
return new Proxy(target, {
get(obj, key) {
track(obj, key)
return Reflect.get(obj, key)
},
set(obj, key, value) {
Reflect.set(obj, key, value)
trigger(obj, key)
return true
}
})
}
ref和reactive的区别:reactive仅适用于对象类型,返回原始对象的Proxy代理;ref适用于所有类型,内部通过.value属性访问,基本类型使用RefImpl类包装,对象类型内部仍调用reactive。
ref与reactive响应式数据封装
实际开发中的选择策略:基本类型用ref,对象/数组用reactive,需要整体替换的对象用ref。ref的可读性可通过ESLint规则强制要求解构时使用toRefs:
import { ref, reactive, toRefs } from 'vue'
// 推荐:基本类型用ref
const count = ref(0)
const loading = ref(false)
// 推荐:响应式表单用reactive
const form = reactive({
username: '',
password: '',
remember: false
})
// 需要整体替换时用ref
const userList = ref([])
userList.value = await fetchUsers() // 直接替换整个数组
// reactive对象解构时用toRefs保持响应性
function useForm() {
const form = reactive({ name: '', email: '' })
return {
...toRefs(form), // 解构后仍是响应式
submit: () => { /* ... */ }
}
}
深层响应与浅层响应的选择:reactive默认深层响应,嵌套对象修改也会触发更新。shallowReactive仅追踪第一层属性,适用于大型数据结构性能优化场景:
import { shallowReactive, shallowRef } from 'vue'
// 大型列表渲染,仅关注数组本身变化
const tableData = shallowReactive([])
// push/splice可触发更新,但修改元素内部属性不触发
tableData.push(newItem) // 响应式
tableData[0].name = 'new' // 不触发,需手动触发
// 需要手动触发时
triggerRef(tableData)
computed计算属性与watch侦听器配置
computed具有缓存特性,依赖未变化时不会重新计算。watch用于响应式数据变化时执行副作用:
import { ref, computed, watch, watchEffect } from 'vue'
const firstName = ref('张')
const lastName = ref('三')
// computed自动追踪依赖
const fullName = computed(() => `${firstName.value}${lastName.value}`)
// 多次访问只计算一次,直到依赖变化
console.log(fullName.value) // 计算
console.log(fullName.value) // 缓存命中
// 带getter和setter的computed
const displayName = computed({
get: () => fullName.value,
set: (val) => {
firstName.value = val.charAt(0)
lastName.value = val.slice(1)
}
})
// watch侦听特定源
watch(fullName, (newVal, oldVal) => {
console.log(`从 ${oldVal} 变为 ${newVal}`)
}, { immediate: false, deep: false })
// watch多源侦听
watch([firstName, lastName], ([newFirst, newLast]) => {
console.log(`${newFirst}${newLast}`)
})
// watchEffect自动收集依赖,立即执行
watchEffect(() => {
console.log(`当前全名: ${fullName.value}`)
})
// watch的flush选项控制执行时机
watch(count, callback, {
flush: 'post' // DOM更新后执行,默认'pre'在更新前
})
watch侦听reactive对象需要deep选项才能检测嵌套属性变化。侦听ref对象则不需要。侦听函数返回值时,Vue3自动判断是否需要deep:
const state = reactive({
user: { name: '', age: 0 },
settings: { theme: 'light' }
})
// 侦听整个reactive对象,默认deep=true
watch(state, (newVal) => {
console.log('state变化', newVal)
})
// 侦听具体属性,需要返回getter函数
watch(() => state.user.name, (newName) => {
console.log('名字变了', newName)
})
// 侦听reactive的嵌套对象,自动deep
watch(() => state.user, (newUser) => {
console.log('用户信息变了', newUser)
}, { deep: true })
自定义组合式函数封装实践
组合式函数命名规范:以use开头,返回响应式数据和方法。以通用分页逻辑为例:
import { ref, reactive, computed } from 'vue'
export function usePagination(fetchFn, options = {}) {
const {
pageSize: defaultPageSize = 20,
immediate = true
} = options
const currentPage = ref(1)
const pageSize = ref(defaultPageSize)
const total = ref(0)
const loading = ref(false)
const data = ref([])
const totalPages = computed(() =>
Math.ceil(total.value / pageSize.value)
)
const hasNext = computed(() => currentPage.value < totalPages.value)
const hasPrev = computed(() => currentPage.value > 1)
async function loadData() {
loading.value = true
try {
const res = await fetchFn({
page: currentPage.value,
pageSize: pageSize.value
})
data.value = res.list
total.value = res.total
} finally {
loading.value = false
}
}
function goToPage(page) {
if (page < 1 || page > totalPages.value) return
currentPage.value = page
loadData()
}
function changeSize(size) {
pageSize.value = size
currentPage.value = 1
loadData()
}
if (immediate) {
loadData()
}
return {
currentPage,
pageSize,
total,
totalPages,
loading,
data,
hasNext,
hasPrev,
goToPage,
changeSize,
refresh: loadData
}
}
在组件中使用封装好的组合式函数:
<script setup>
import { usePagination } from '@/composables/usePagination'
import { fetchOrderList } from '@/api/orders'
const {
currentPage, pageSize, total, loading,
data: orderList, goToPage, changeSize, refresh
} = usePagination(fetchOrderList, { pageSize: 15 })
</script>
<template>
<el-table :data="orderList" v-loading="loading">
<!-- 列定义 -->
</el-table>
<el-pagination
v-model:current-page="currentPage"
v-model:page-size="pageSize"
:total="total"
@current-change="goToPage"
@size-change="changeSize"
/>
</template>
组件逻辑复用模式设计
组合式函数相比mixins的核心优势:数据来源明确、无命名冲突、类型推导友好。多个组合式函数在同一组件中使用时,各逻辑块独立且可追踪:
// 组合多个composable
import { useFetch } from '@/composables/useFetch'
import { useDebounce } from '@/composables/useDebounce'
import { useLocalStorage } from '@/composables/useLocalStorage'
// 在组件中组合
const { data, error, execute } = useFetch('/api/users')
const debouncedSearch = useDebounce(searchQuery, 300)
const savedState = useLocalStorage('app-state', { filter: 'all' })
组合式函数之间可嵌套调用。useFetch内部可使用useDebounce实现防抖请求,usePagination内部可使用useFetch获取数据。这种组合模式让逻辑层次清晰,每层只关注单一职责。
TypeScript场景下,组合式函数的返回值类型推导至关重要。使用泛型约束确保类型安全。对于ref返回值,调用方获得Ref
原创文章,作者:小编,如若转载,请注明出处:https://www.yunthe.com/vue3compositionapi-zu-he-shi-han-shu-feng-zhuang-yu-xiang/