Vue3组合式API进阶:响应式系统原理与自定义Hook封装实践

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/

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

相关推荐