Vue3的组合式API(Composition API)是前端工程化体系中的一次重要演进。相比Options API,组合式API在逻辑复用和类型推导上有明显优势。本文从响应式系统底层原理出发,到自定义Hook封装模式,梳理组合式API在实际项目中的进阶用法。
Vue3响应式系统核心原理剖析
Vue3的响应式系统基于ES6 Proxy实现,替代了Vue2中基于Object.defineProperty的方案。Proxy能拦截对象的所有操作,包括属性新增和删除,这是defineProperty无法做到的。
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
}
})
}
实际实现中,Vue3还处理了嵌套对象的递归代理、Map/Set集合类型的响应式、ref的依赖追踪等复杂场景。理解这个简化模型,有助于排查”修改了数据但视图没更新”的问题——通常是修改了非响应式数据,或者在reactive对象上直接替换了整个对象。
ref与reactive的底层实现差异与选择
reactive只能代理对象类型,对原始值(string、number、boolean)无法使用。ref通过包装一层对象来解决这个问题:
function ref(value) {
return {
get value() {
track(this, 'value')
return value
},
set value(newVal) {
value = newVal
trigger(this, 'value')
}
}
}
实际使用中的选择标准:对象类型数据用reactive,原始值用ref。当整个对象需要重新赋值时(如从API获取数据后替换),必须用ref,因为reactive对象重新赋值会丢失响应性:
// 错误写法:响应性丢失
const state = reactive({ list: [] })
state = { list: [1, 2, 3] } // state不再是响应式代理
// 正确写法:用ref或修改属性
const state = ref({ list: [] })
state.value = { list: [1, 2, 3] } // 响应性保持
// 或者直接修改属性
const state = reactive({ list: [] })
state.list = [1, 2, 3] // 响应性保持
模板中ref会自动解包,不需要写.value。但在JavaScript逻辑中必须通过.value访问。这是初学者最容易遗漏的点。
computed与watch的高级用法
computed创建一个缓存计算属性,只在依赖变化时重新计算。与methods的区别在于缓存机制——computed的结果会被缓存,多次读取时不会重复计算:
import { ref, computed } from 'vue'
const price = ref(100)
const quantity = ref(3)
// 缓存计算,只有price或quantity变化时才重新计算
const total = computed(() => price.value * quantity.value)
// 可写computed
const fullName = computed({
get() {
return `${firstName.value} ${lastName.value}`
},
set(newVal) {
[firstName.value, lastName.value] = newVal.split(' ')
}
})
watch用于监听数据变化执行副作用。watchEffect是自动收集依赖的版本,不需要显式声明监听源:
import { ref, watch, watchEffect } from 'vue'
const userId = ref(1)
// watch:需要显式声明监听源
watch(userId, (newVal, oldVal) => {
fetchUserProfile(newVal)
}, { immediate: true })
// watchEffect:自动收集依赖
watchEffect(() => {
// 自动追踪内部使用的响应式数据
fetchUserProfile(userId.value)
})
// watch多个数据源
watch([firstName, lastName], ([newFirst, newLast], [oldFirst, oldLast]) => {
console.log(`${oldFirst} ${oldLast} -> ${newFirst} ${newLast}`)
})
// 深度监听
watch(
() => state.value,
(newVal) => {
// state对象内部任何属性变化都触发
},
{ deep: true, flush: 'post' }
)
flush选项控制回调执行时机。pre(默认)在组件更新前执行,post在组件更新后执行(可以访问更新后的DOM),sync同步执行。
自定义Hook封装模式与最佳实践
自定义Hook是组合式API最大的价值所在。一个好的Hook应该职责单一、返回值清晰、可独立测试。以一个分页数据Hook为例:
// hooks/usePagination.js
import { ref, computed, onMounted } 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 data = ref([])
const loading = ref(false)
const error = ref(null)
const totalPages = computed(() =>
Math.ceil(total.value / pageSize.value)
)
async function loadData() {
loading.value = true
error.value = null
try {
const result = await fetchFn({
page: currentPage.value,
size: pageSize.value
})
data.value = result.items
total.value = result.total
} catch (err) {
error.value = err
console.error('Pagination fetch error:', err)
} 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) {
onMounted(loadData)
}
return {
currentPage,
pageSize,
total,
totalPages,
data,
loading,
error,
loadData,
goToPage,
changeSize
}
}
使用方式:
import { usePagination } from '@/hooks/usePagination'
import { fetchUserList } from '@/api/users'
const {
currentPage, total, totalPages,
data: userList, loading,
goToPage, changeSize
} = usePagination(fetchUserList, { pageSize: 15 })
TypeScript类型在组合式API中的应用
组合式API与TypeScript实战结合时,类型推导是关键优势。defineProps和defineEmits支持泛型写法:
<script setup lang="ts">
interface User {
id: number
name: string
email: string
}
// Props类型定义
const props = defineProps<{
user: User
editable?: boolean
}>()
// 带默认值的Props
const props = withDefaults(defineProps<{
user: User
editable?: boolean
}>(), {
editable: false
})
// Emits类型定义
const emit = defineEmits<{
(e: 'update', user: User): void
(e: 'delete', id: number): void
}>()
// ref类型
const list = ref<User[]>([])
const currentUser = ref<User | null>(null)
</script>
组件库设计中,Hook的返回值类型应该显式导出,方便使用方获得类型提示。对于跨端小程序开发或Flutter移动端迁移的场景,Hook模式比组件继承更灵活,逻辑层可以跨端复用。
原创文章,作者:小编,如若转载,请注明出处:https://www.yunthe.com/vue3-zu-he-shi-api-jin-jie-xiang-ying-shi-xi-tong-yuan-li/