Vue3组合式API深度实战:自定义Hook封装模式与响应式数据流管理

Vue3组合式API的设计动机与核心概念

Vue3组合式API(Composition API)解决了Options API在复杂组件中逻辑分散的问题。在Vue2的Options API中,同一个功能的data、methods、computed分散在不同选项中,组件超过300行后维护成本急剧上升。组合式API允许将相关逻辑组织在一起,通过自定义Hook(Composables)实现逻辑复用。

组合式API的核心是ref和reactive两个响应式API。ref用于包装基本类型值,reactive用于包装对象。TypeScript实战中,ref通过泛型推断类型,reactive自动解包嵌套属性。Web性能优化的角度,组合式API的编译产物比Options API更小,Tree-shaking效果更好。

ref与reactive的选择策略与陷阱

ref和reactive的选择是Vue3开发中的高频问题。基本原则:基本类型用ref,对象/数组用reactive。但实际开发中有更多细节需要注意。

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

// ref:基本类型,需要.value访问
const count = ref(0)
count.value++
const userName = ref<string>('')
const userList = ref<User[]>([])

// reactive:对象类型,直接访问属性
const state = reactive({
  loading: false,
  data: [] as Product[],
  error: null as Error | null
})
state.loading = true

// 常见陷阱:reactive解构后失去响应性
const { loading, data } = state  // 失去响应性
// 正确做法:使用toRefs保持响应性
import { toRefs } from 'vue'
const { loading, data } = toRefs(state)

reactive的另一个陷阱是整体替换对象会丢失响应性。当从API获取数据后需要赋值整个对象时,应使用Object.assign或逐属性赋值,而非直接替换reactive变量:

// 错误:替换后不再是Proxy对象
state = { loading: false, data: newProducts, error: null }

// 正确方式一:Object.assign
Object.assign(state, { loading: false, data: newProducts, error: null })

// 正确方式二:用ref包装对象
const state = ref({ loading: false, data: [] })
state.value = { loading: false, data: newProducts }

自定义Hook封装模式与实战

自定义Hook是Vue3逻辑复用的标准模式。一个设计良好的Hook应该遵循单一职责原则,接收响应式输入,返回响应式输出,并正确处理生命周期清理。以下是一个完整的CRUD数据请求Hook:

// composables/useFetch.ts
import { ref, watch, isRef, unref, onUnmounted } from 'vue'

interface UseFetchOptions<T> {
  immediate?: boolean
  initialData?: T
  onSuccess?: (data: T) => void
  onError?: (error: Error) => void
}

export function useFetch<T>(
  url: string | Ref<string>,
  options: UseFetchOptions<T> = {}
) {
  const { immediate = true, initialData } = options
  const data = ref<T | undefined>(initialData)
  const error = ref<Error | null>(null)
  const loading = ref(false)

  let abortController: AbortController | null = null

  async function execute() {
    loading.value = true
    error.value = null
    abortController?.abort()
    abortController = new AbortController()

    try {
      const response = await fetch(unref(url), {
        signal: abortController.signal,
        headers: { 'Content-Type': 'application/json' }
      })
      if (!response.ok) throw new Error(`HTTP ${response.status}`)
      data.value = await response.json() as T
      options.onSuccess?.(data.value)
    } catch (e) {
      if (e.name !== 'AbortError') {
        error.value = e as Error
        options.onError?.(error.value)
      }
    } finally {
      loading.value = false
    }
  }

  if (isRef(url)) {
    watch(url, execute)
  }

  onUnmounted(() => abortController?.abort())
  if (immediate) execute()

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

使用Hook的组件代码极其简洁,逻辑复用的效果立竿见影:

// 组件中使用
const { data, error, loading, refresh } = useFetch<Product[]>('/api/products', {
  onSuccess: (data) => console.log(`加载到 ${data.length} 条数据`)
})

响应式数据流管理与watch使用规范

复杂组件中响应式数据的流转通过computed和watch实现。computed用于派生状态,watch用于副作用。前端工程化中,watch的正确使用直接影响Web性能优化效果。

import { ref, computed, watch, watchEffect } from 'vue'
import { debounce } from 'lodash-es'

const searchQuery = ref('')
const page = ref(1)
const pageSize = ref(20)
const allItems = ref<Item[]>([])

// computed:派生状态,自动缓存
const filteredItems = computed(() => {
  if (!searchQuery.value) return allItems.value
  return allItems.value.filter(item => item.name.includes(searchQuery.value))
})

const pagedItems = computed(() => {
  const start = (page.value - 1) * pageSize.value
  return filteredItems.value.slice(start, start + pageSize.value)
})

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

// watch:副作用,带防抖
const debouncedSearch = debounce((q: string) => {
  page.value = 1
  console.log('执行搜索:', q)
}, 300)

watch(searchQuery, (newVal) => debouncedSearch(newVal))

// watchEffect:自动收集依赖
watchEffect(() => {
  document.title = `列表 - 第${page.value}页 / 共${totalPages.value}页`
})

// watch多个数据源
watch([page, pageSize], ([newPage, newSize], [oldPage, oldSize]) => {
  if (newPage !== oldPage || newSize !== oldSize) {
    console.log(`分页变化: ${oldPage},${oldSize} -> ${newPage},${newSize}`)
  }
}, { flush: 'post' })

watch的flush选项影响执行时机:默认’pre’在组件更新前执行,’post’在DOM更新后执行(适合需要访问更新后DOM的场景),’sync’同步执行(性能影响大,不推荐)。

组件库设计中的组合式API实践

响应式布局和组件库设计中,组合式API的灵活性更加突出。以可拖拽面板组件为例,将拖拽逻辑封装为useDraggable Hook,与面板UI组件解耦:

// composables/useDraggable.ts
export function useDraggable(target: Ref<HTMLElement | null>) {
  const isDragging = ref(false)
  const position = reactive({ x: 0, y: 0 })
  const offset = reactive({ x: 0, y: 0 })

  function onMouseDown(e: MouseEvent) {
    isDragging.value = true
    offset.x = e.clientX - position.x
    offset.y = e.clientY - position.y
    document.addEventListener('mousemove', onMouseMove)
    document.addEventListener('mouseup', onMouseUp)
  }

  function onMouseMove(e: MouseEvent) {
    if (!isDragging.value) return
    position.x = e.clientX - offset.x
    position.y = e.clientY - offset.y
  }

  function onMouseUp() {
    isDragging.value = false
    document.removeEventListener('mousemove', onMouseMove)
    document.removeEventListener('mouseup', onMouseUp)
  }

  onUnmounted(() => {
    document.removeEventListener('mousemove', onMouseMove)
    document.removeEventListener('mouseup', onMouseUp)
  })

  return { isDragging, position, onMouseDown }
}

Vue3组合式API通过自定义Hook模式,将组件逻辑从模板和样式中剥离,实现了高内聚低耦合的代码组织。TypeScript类型推导在组合式API中表现更好,ref和reactive的响应式系统相比Vue2的defineProperty方案,对数组和Map/Set的支持更完善,避免了Vue2中$set、$delete等特殊API的使用。在实际跨端小程序开发中,组合式API同样适用,逻辑层Hook可直接复用。

原创文章,作者:小编,如若转载,请注明出处:https://www.yunthe.com/vue3-zu-he-shi-api-shen-du-shi-zhan-zi-ding-yi-hook-feng/

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

相关推荐