Vue3组合式API实战:响应式系统原理与组件设计模式深度解析

Vue3的组合式API(Composition API)重构了组件逻辑的组织方式,解决了Options API在复杂组件中逻辑分散的问题。理解响应式系统的工作原理,是编写高性能Vue3应用的基础。本文从响应式机制到composable函数设计,覆盖工程实践中最核心的API用法和设计模式。

Vue3响应式系统核心原理:Proxy与依赖追踪

Vue3使用ES6 Proxy替代了Vue2的Object.defineProperty实现响应式。Proxy能拦截对象的所有属性操作,包括新增和删除属性,无需像Vue2那样使用$set。

响应式系统的核心是三个概念的协作:reactive(代理对象)、effect(副作用函数)和track/trigger(依赖收集与触发)。

// reactive的简化实现原理
const targetMap = new WeakMap()

function reactive(target) {
    const handler = {
        get(obj, key, receiver) {
            const result = Reflect.get(obj, key, receiver)
            track(target, key)  // 依赖收集
            if (typeof result === 'object' && result !== null) {
                return reactive(result)  // 深层代理
            }
            return result
        },
        set(obj, key, value, receiver) {
            const oldValue = obj[key]
            const result = Reflect.set(obj, key, value, receiver)
            if (oldValue !== value) {
                trigger(target, key)  // 触发更新
            }
            return result
        }
    }
    return new Proxy(target, handler)
}

function track(target, key) {
    if (!activeEffect) return
    let depsMap = targetMap.get(target)
    if (!depsMap) {
        targetMap.set(target, (depsMap = new Map()))
    }
    let dep = depsMap.get(key)
    if (!dep) {
        depsMap.set(key, (dep = new Set()))
    }
    dep.add(activeEffect)
}

ref和reactive的区别在于:reactive代理整个对象,适合对象/数组;ref通过.value访问,适合基本类型和需要整体替换的场景。ref内部对对象类型也会调用reactive进行深层代理。

import { ref, reactive, isRef, isReactive } from 'vue'

// 基本类型用ref
const count = ref(0)
console.log(count.value)  // 0
count.value++
console.log(count.value)  // 1

// 对象/数组用reactive
const state = reactive({
    user: { name: '张三', age: 28 },
    list: [1, 2, 3]
})
state.user.name = '李四'  // 自动触发更新

// 判断类型
console.log(isRef(count))      // true
console.log(isReactive(state)) // true

computed与watch的精确控制

computed是带缓存的响应式计算属性。只有当依赖的响应式数据变化时才会重新计算,否则返回缓存值。

import { ref, computed, watch, watchEffect } from 'vue'

const price = ref(99.9)
const quantity = ref(3)
const discount = ref(0.8)

// computed默认只读,可写版本用get/set
const total = computed({
    get: () => price.value * quantity.value * discount.value,
    set: (newVal) => {
        // 反向推导折扣
        discount.value = newVal / (price.value * quantity.value)
    }
})

// watch监听特定响应式源
watch(
    () => state.user.age,
    (newAge, oldAge) => {
        console.log(`年龄从 ${oldAge} 变为 ${newAge}`)
    },
    {
        immediate: true,      // 立即执行一次
        deep: true,           // 深度监听(对象内部变化)
        flush: 'post',        // 在DOM更新后执行
    }
)

// watchEffect自动收集依赖,无需显式声明
watchEffect(() => {
    console.log(`当前总价: ${total.value}`)
})

watch与watchEffect的选择原则:需要访问旧值或需要明确指定监听源时用watch;只需副作用自动追踪依赖时用watchEffect。watchEffect的清理函数通过onCleanup注册:

watchEffect((onCleanup) => {
    const timer = setInterval(() => {
        console.log('polling...', count.value)
    }, 1000)

    onCleanup(() => {
        clearInterval(timer)
    })
})

Composable函数设计模式与最佳实践

Composable函数是组合式API的核心复用单元。命名以use开头,接受响应式输入并返回响应式输出。一个设计良好的composable应该做到:输入输出都是响应式的、不产生副作用、可独立测试。

// useMouse.ts - 鼠标位置追踪
import { ref, onMounted, onUnmounted, type Ref } from 'vue'

interface MousePosition {
    x: Ref<number>
    y: Ref<number>
}

export function useMouse(): MousePosition {
    const x = ref(0)
    const y = ref(0)

    const update = (e: MouseEvent) => {
        x.value = e.pageX
        y.value = e.pageY
    }

    onMounted(() => {
        window.addEventListener('mousemove', update)
    })

    onUnmounted(() => {
        window.removeEventListener('mousemove', update)
    })

    return { x, y }
}

// useFetch.ts - 数据请求封装
import { ref, watch, isRef, unref, type Ref } from 'vue'

export function useFetch<T>(
    url: string | Ref<string>,
    options?: RequestInit
) {
    const data = ref<T | null>(null)
    const error = ref<string | null>(null)
    const loading = ref(false)

    async function doFetch() {
        loading.value = true
        error.value = null
        data.value = null

        try {
            const res = await fetch(unref(url), options)
            if (!res.ok) throw new Error(`HTTP ${res.status}`)
            data.value = await res.json() as T
        } catch (e) {
            error.value = (e as Error).message
        } finally {
            loading.value = false
        }
    }

    // 如果url是响应式引用,自动重新请求
    if (isRef(url)) {
        watch(url, doFetch, { immediate: true })
    } else {
        doFetch()
    }

    return { data, error, loading, refresh: doFetch }
}

在组件中使用composable:

<script setup lang="ts">
import { useMouse, useFetch } from './composables'

interface User {
    id: number
    name: string
    email: string
}

const { x, y } = useMouse()
const { data: users, loading, error, refresh } = useFetch<User[]>(
    'https://api.example.com/users'
)
</script>

<template>
    <div>
        <p>鼠标位置: ({{ x }}, {{ y }})</p>
        <button @click="refresh" :disabled="loading">
            {{ loading ? '加载中' : '刷新' }}
        </button>
        <ul v-if="users">
            <li v-for="user in users" :key="user.id">
                {{ user.name }} - {{ user.email }}
            </li>
        </ul>
        <p v-if="error" class="error">{{ error }}</p>
    </div>
</template>

组件通信模式与provide/inject类型安全

组合式API下,组件通信推荐使用provide/inject配合InjectionKey实现类型安全。相比props/emit的逐层传递,provide/inject适合跨层级共享状态。

// keys.ts - 定义类型安全的InjectionKey
import type { InjectionKey, Ref } from 'vue'
import { provide, inject, ref } from 'vue'

export interface ThemeContext {
    theme: Ref<'light' | 'dark'>
    toggle: () => void
}

export const ThemeKey: InjectionKey<ThemeContext> = Symbol('theme')

// Provider组件
export function provideTheme() {
    const theme = ref<'light' | 'dark'>('light')
    const toggle = () => {
        theme.value = theme.value === 'light' ? 'dark' : 'light'
    }
    provide(ThemeKey, { theme, toggle })
    return { theme, toggle }
}

// Consumer组件
export function useTheme(): ThemeContext {
    const ctx = inject(ThemeKey)
    if (!ctx) {
        throw new Error('useTheme must be used within a ThemeProvider')
    }
    return ctx
}

性能优化:shallowRef与markRaw的使用场景

Vue3默认对reactive对象进行深层代理,大对象或无需响应式的数据会带来不必要的性能开销。shallowRef和markRaw可以跳过响应式代理。

import { shallowRef, markRaw, triggerRef } from 'vue'

// shallowRef:只有.value替换时才触发更新,不代理内部属性
const largeList = shallowRef<HeavyItem[]>([])
// 修改内部属性不会触发更新
largeList.value.push(newItem)  // 不触发
// 需手动触发
triggerRef(largeList)
// 或整体替换
largeList.value = [...largeList.value, newItem]  // 触发

// markRaw:标记对象永不转为响应式
const mapInstance = markRaw(new HeavyMap())
const chartConfig = reactive({
    chart: mapInstance  // 不会被代理
})

典型场景:ECharts实例、Map/Set集合、第三方库对象实例。这些对象不需要Vue的响应式追踪,标记为markRaw可避免Proxy包装导致的性能问题和潜在的循环引用。

组合式API的设计哲学是将组件逻辑按功能聚合而非按选项分散。通过composable函数提取复用逻辑,配合TypeScript类型推导和InjectionKey类型安全,可以构建出结构清晰、可维护性强的Vue3应用。实际项目中建议将composable按领域分目录管理,每个文件只导出一个函数,保持单一职责。

原创文章,作者:小编,如若转载,请注明出处:https://www.yunthe.com/vue3-zu-he-shi-api-shi-zhan-xiang-ying-shi-xi-tong-yuan-li/

(0)
小编小编
上一篇 11小时前
下一篇 11小时前

相关推荐