Vue3 Composition API组合式函数封装与响应式系统原理实战

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/

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

相关推荐