Vue3组合式API不只是语法糖
Vue3组合式API(Composition API)的核心价值不在于写法不同,而在于它从根本上改变了状态管理和逻辑复用的方式。当项目规模超过50个组件后,选项式API的mixins命名冲突、逻辑分散等问题会集中爆发。本文从响应式原理到大型项目架构优化,拆解组合式API的实战要点。
响应式系统原理:Proxy代理与依赖追踪
Vue3用Proxy替代了Vue2的Object.defineProperty,解决了数组索引监听和动态属性添加的限制。核心机制:
// Vue3响应式简化实现
const targetMap = new WeakMap()
let activeEffect = null
function reactive(target) {
return new Proxy(target, {
get(obj, key, receiver) {
// 依赖追踪:记录当前effect与属性的映射
track(obj, key)
const result = Reflect.get(obj, key, receiver)
// 深层响应式:对嵌套对象递归代理
return typeof result === 'object' ? reactive(result) : result
},
set(obj, key, value, receiver) {
const oldValue = obj[key]
const result = Reflect.set(obj, key, value, receiver)
if (oldValue !== value) {
// 触发更新
trigger(obj, key)
}
return result
}
})
}
function track(target, key) {
if (!activeEffect) return
let depsMap = targetMap.get(target)
if (!depsMap) {
depsMap = new Map()
targetMap.set(target, depsMap)
}
let deps = depsMap.get(key)
if (!deps) {
deps = new Set()
depsMap.set(key, deps)
}
deps.add(activeEffect)
}
function trigger(target, key) {
const depsMap = targetMap.get(target)
if (!depsMap) return
const effects = depsMap.get(key)
effects && effects.forEach(effect => effect())
}
理解这个原理后,就能明白几个常见坑:
1. 解构丢失响应式
const state = reactive({ count: 0, name: 'test' })
// 错误:解构后变成普通变量
const { count } = state // count不是响应式的
// 正确方案1:toRefs
const { count } = toRefs(state) // count是ref
// 正确方案2:直接使用reactive对象
// template中用 state.count
2. reactive的局限性
reactive只对对象类型生效,无法包裹基本类型。对基本类型需要用ref:
const count = ref(0)
// ref内部通过value属性实现代理
// template中自动解包,不需要.value
// JS中必须用 .value 访问
Composables:逻辑复用的正确姿势
组合式API最大的架构收益是Composables替代mixins,消除命名冲突和来源不透明的痛点:
// composables/usePagination.js
import { ref, computed } from 'vue'
export function usePagination(fetchFn, pageSize = 20) {
const currentPage = ref(1)
const total = ref(0)
const loading = ref(false)
const data = ref([])
const totalPages = computed(() => Math.ceil(total.value / pageSize))
async function goToPage(page) {
if (page < 1 || page > totalPages.value) return
currentPage.value = page
loading.value = true
try {
const result = await fetchFn({ page, pageSize })
data.value = result.list
total.value = result.total
} finally {
loading.value = false
}
}
function nextPage() { goToPage(currentPage.value + 1) }
function prevPage() { goToPage(currentPage.value - 1) }
// 首次加载
goToPage(1)
return {
currentPage, total, totalPages,
loading, data, goToPage, nextPage, prevPage
}
}
// composables/useInfiniteScroll.js
import { ref, onMounted, onUnmounted } from 'vue'
export function useInfiniteScroll(fetchFn, threshold = 200) {
const items = ref([])
const loading = ref(false)
const finished = ref(false)
let page = 1
async function loadMore() {
if (loading.value || finished.value) return
loading.value = true
const result = await fetchFn({ page })
items.value.push(...result.list)
finished.value = result.list.length === 0
page++
loading.value = false
}
function onScroll() {
const { scrollTop, scrollHeight, clientHeight } = document.documentElement
if (scrollHeight - scrollTop - clientHeight < threshold) {
loadMore()
}
}
onMounted(() => window.addEventListener('scroll', onScroll))
onUnmounted(() => window.removeEventListener('scroll', onScroll))
return { items, loading, finished, loadMore }
}
在组件中使用:
// UserList.vue
import { usePagination } from '@/composables/usePagination'
import { api } from '@/api'
const { data, currentPage, totalPages, loading, nextPage } = usePagination(
api.getUserList,
15
)
大型项目性能优化:虚拟滚动与懒加载
虚拟滚动:处理万级列表渲染
// components/VirtualList.vue
<template>
<div class="virtual-list" :style="{ height: containerHeight + 'px' }" @scroll="onScroll">
<div class="virtual-list-phantom" :style="{ height: totalHeight + 'px' }"></div>
<div class="virtual-list-content" :style="{ transform: `translateY(${offsetY}px)` }">
<div v-for="item in visibleData" :key="item.id" class="virtual-list-item">
<slot :item="item" />
</div>
</div>
</div>
</template>
<script setup>
import { ref, computed, onMounted } from 'vue'
const props = defineProps({
data: { type: Array, required: true },
itemHeight: { type: Number, default: 50 },
containerHeight: { type: Number, default: 600 }
})
const scrollTop = ref(0)
const visibleCount = computed(() =>
Math.ceil(props.containerHeight / props.itemHeight) + 2
)
const startIndex = computed(() =>
Math.floor(scrollTop.value / props.itemHeight)
)
const visibleData = computed(() => {
const start = Math.max(0, startIndex.value - 1)
const end = start + visibleCount.value
return props.data.slice(start, end)
})
const totalHeight = computed(() => props.data.length * props.itemHeight)
const offsetY = computed(() => startIndex.value * props.itemHeight)
function onScroll(e) {
scrollTop.value = e.target.scrollTop
}
</script>
路由级懒加载与组件预加载
// router/index.js
const routes = [
{
path: '/dashboard',
component: () => import('@/views/Dashboard.vue')
},
{
path: '/analytics',
component: () => import(/* webpackPrefetch: true */ '@/views/Analytics.vue')
}
]
// 鼠标悬停时预加载
const router = createRouter({ routes, history: createWebHistory() })
router.beforeEach((to) => {
if (to.meta.preload) {
import(`@/views/${to.meta.preload}.vue`)
}
})
Pinia状态管理:从Vuex迁移的实践
Pinia是Vue3官方推荐的状态管理方案,相比Vuex去掉了mutations这个冗余层:
// stores/user.js
import { defineStore } from 'pinia'
export const useUserStore = defineStore('user', () => {
const userInfo = ref(null)
const token = ref(localStorage.getItem('token') || '')
const permissions = computed(() => userInfo.value?.permissions || [])
async function login(credentials) {
const res = await api.login(credentials)
token.value = res.token
userInfo.value = res.user
localStorage.setItem('token', res.token)
}
function logout() {
token.value = ''
userInfo.value = null
localStorage.removeItem('token')
}
return { userInfo, token, permissions, login, logout }
})
// 组件中使用
const userStore = useUserStore()
// 直接解构state(不会丢失响应式,因为Pinia用storeToRefs)
const { userInfo, permissions } = storeToRefs(userStore)
Vue3组合式API配合Composables、Pinia、虚拟滚动这些工程化方案,可以让大型前端项目在开发效率和运行性能之间找到最佳平衡点。
原创文章,作者:小编,如若转载,请注明出处:https://www.yunthe.com/vue3-zu-he-shi-api-shen-du-shi-zhan-xiang-ying-shi-yuan-li/