Vue 3.5 Composition API响应式原理与自定义Hook设计实战

Vue 3的Composition API通过setup函数和一组响应式API重新组织组件逻辑,解决Options API在复杂组件中逻辑分散的问题。Vue 3.5版本对响应式系统做了内部重构,依赖追踪采用双向链表结构替代原有的Set集合,内存占用降低约56%,大列表渲染性能显著提升。理解ref、reactive、computed和watch的底层机制是编写高质量自定义Hook和调试响应式问题的前提。

ref与reactive响应式代理机制对比

ref通过RefImpl类包装原始值,通过get value和set value拦截器实现依赖收集和触发。reactive通过Proxy代理整个对象,递归拦截属性访问。两者的核心差异在于ref适用于原始类型和需要整体替换的引用类型,reactive适用于需要保持引用稳定性的对象。

// ref与reactive的实现差异演示
import { ref, reactive, watchEffect } from 'vue'

// ref:包装原始值,访问需要.value
const count = ref(0)
console.log(count.value) // 0
count.value++ // 触发依赖更新

// reactive:代理对象,直接访问属性
const state = reactive({ name: 'Vue', version: 3 })
state.version = 3.5 // 触发依赖更新

// reactive的陷阱:解构丢失响应性
const { name } = state // name不再是响应式
// 解决方案:使用toRefs
import { toRefs } from 'vue'
const { name: reactiveName } = toRefs(state) // reactiveName保持响应性

// ref的模板自动解包:在template中不需要.value
// <div>{{ count }}</div> 正确,不需要count.value

// 深层响应:reactive自动递归代理嵌套对象
const nested = reactive({
  user: { profile: { age: 25 } }
})
nested.user.profile.age = 26 // 触发更新

Vue 3.5引入了ReactiveEffect双向链表优化,每个副作用函数和维护其依赖的dep对象直接关联,避免Set遍历开销。当watchEffect或computed的回调执行时,被访问的ref/reactive属性自动将当前Effect注册为依赖;当属性变更时,通过dep直接定位并执行关联的Effect,查找复杂度从O(n)降至O(1)。

computed缓存机制与watch回调时机

computed创建时返回ComputedRefImpl实例,内部维护_dirty标志位控制缓存。首次访问时执行getter计算并缓存结果,同时将自身注册为依赖项的订阅者。依赖变更时设置_dirty为true,下次访问才重新计算,实现惰性求值。

// computed缓存与watch配置详解
import { ref, computed, watch } from 'vue'

const firstName = ref('John')
const lastName = ref('Doe')

// computed缓存:依赖未变时不重新计算
const fullName = computed(() => {
  console.log('computed executed') // 仅在依赖变化后首次访问时打印
  return `${firstName.value} ${lastName.value}`
})

console.log(fullName.value) // 打印"computed executed",返回"John Doe"
console.log(fullName.value) // 不打印,返回缓存值"John Doe"
firstName.value = 'Jane'
console.log(fullName.value) // 打印"computed executed",返回"Jane Doe"

// watch选项配置
const searchText = ref('')

// flush: 'pre'(默认)在组件更新前执行,'post'在更新后,'sync'同步执行
watch(searchText, (newVal, oldVal, onCleanup) => {
  const controller = new AbortController()
  fetch(`/api/search?q=${newVal}`, { signal: controller.signal })
    .then(res => res.json())
    .then(data => console.log(data))

  // onCleanup注册清理函数,下次回调前自动执行
  onCleanup(() => controller.abort())
}, {
  immediate: true,    // 首次执行时立即触发回调
  deep: false,        // 深度监听(仅对reactive对象有效)
  flush: 'pre',       // 回调执行时机
})

// watchEffect:自动收集依赖,无需显式声明
watchEffect(async (onCleanup) => {
  // 自动追踪searchText.value作为依赖
  const data = await fetchData(searchText.value)
  // 副作用逻辑
})

watch的flush: ‘pre’选项在大多数场景下是正确的选择,DOM更新前执行回调避免布局抖动。需要访问更新后的DOM时使用flush: ‘post’。flush: ‘sync’在依赖变更后立即同步执行,可能引发性能问题,仅在确有需求时使用。

自定义Hook设计模式与复用实践

自定义Hook本质是复用响应式逻辑的函数,命名以use开头,返回ref或包含ref的普通对象。设计原则包括:保持Hook职责单一、所有响应式状态通过返回值暴露、副作用在Hook内部通过onMounted等生命周期钩子管理。

// 自定义Hook:通用分页逻辑复用
import { ref, computed, watch, readonly } from 'vue'

export function usePagination(fetchFn, options = {}) {
  const currentPage = ref(options.defaultPage ?? 1)
  const pageSize = ref(options.defaultSize ?? 20)
  const total = ref(0)
  const data = ref([])
  const loading = ref(false)
  const error = ref(null)

  const totalPages = computed(() =>
    Math.ceil(total.value / pageSize.value)
  )

  const hasNext = computed(() => currentPage.value < totalPages.value)
  const hasPrev = computed(() => currentPage.value > 1)

  async function loadPage() {
    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
    } finally {
      loading.value = false
    }
  }

  function nextPage() {
    if (hasNext.value) {
      currentPage.value++
      loadPage()
    }
  }

  function prevPage() {
    if (hasPrev.value) {
      currentPage.value--
      loadPage()
    }
  }

  // 监听分页参数变化自动加载
  watch([currentPage, pageSize], loadPage, { immediate: true })

  return {
    currentPage: readonly(currentPage),
    pageSize: readonly(pageSize),
    total: readonly(total),
    totalPages,
    data: readonly(data),
    loading: readonly(loading),
    error: readonly(error),
    hasNext,
    hasPrev,
    nextPage,
    prevPage,
    loadPage,
  }
}

// 组件中使用
// const { data, loading, nextPage, hasNext } = usePagination(fetchUsers, {
//   defaultPage: 1, defaultSize: 10
// })

上述usePagination Hook封装了分页状态管理和数据加载逻辑,组件只需提供fetchFn即可获得完整的分页能力。返回值使用readonly包裹ref防止外部直接修改内部状态,所有状态变更通过暴露的方法执行。这种设计模式在表格、列表、瀑布流等场景可直接复用,显著减少组件内的重复逻辑。

自定义Hook组合能力是Composition API的核心优势。usePagination可以与useDebounce Hook组合实现搜索防抖分页,与useIntersectionObserver Hook组合实现无限滚动分页。每个Hook保持独立测试能力,不依赖组件上下文,单元测试更简洁。

原创文章,作者:小编,如若转载,请注明出处:https://www.yunthe.com/vue35compositionapi-xiang-ying-shi-yuan-li-yu-zi-ding-yi/

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

相关推荐