Vue3组合式API深度实战:响应式原理剖析与可复用组合式函数设计

Vue3的组合式API(Composition API)改变了组件逻辑的组织方式,解决了Options API在复杂组件中逻辑分散的问题。本文通过代码示例剖析响应式系统原理,并演示如何设计可复用的组合式函数。

响应式系统核心:ref与reactive的实现差异

Vue3提供两种响应式API:ref和reactive。reactive基于Proxy实现,对对象进行深度代理,访问和修改属性时自动触发依赖追踪和更新。ref通过包装值类型并拦截value属性的get和set实现响应式。

reactive的实现原理:

// Vue3 reactive简化实现
const reactiveMap = new WeakMap()

function reactive(target) {
  if (reactiveMap.has(target)) {
    return reactiveMap.get(target)
  }
  const proxy = new Proxy(target, {
    get(obj, key, receiver) {
      track(obj, key)  // 依赖收集
      const result = Reflect.get(obj, key, receiver)
      if (typeof result === 'object' && result !== null) {
        return reactive(result)  // 深度响应式
      }
      return result
    },
    set(obj, key, value, receiver) {
      const result = Reflect.set(obj, key, value, receiver)
      trigger(obj, key)  // 触发更新
      return result
    }
  })
  reactiveMap.set(target, proxy)
  return proxy
}

ref的简化实现:

function ref(value) {
  return {
    get value() {
      track(this, 'value')
      return value
    },
    set value(newVal) {
      if (newVal !== value) {
        value = newVal
        trigger(this, 'value')
      }
    }
  }
}

选择原则:基本类型用ref,对象类型用reactive。模板中ref自动解包,脚本中需要通过.value访问。

组合式函数设计:状态管理与逻辑复用

组合式函数(Composables)是Vue3逻辑复用的核心模式。以一个分页数据加载组合式函数为例:

// composables/usePagination.js
import { ref, reactive, computed } from 'vue'

export function usePagination(fetchFn, options = {}) {
  const {
    pageSize = 20,
    initialPage = 1
  } = options

  const state = reactive({
    data: [],
    total: 0,
    currentPage: initialPage,
    loading: false,
    error: null
  })

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

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

  async function loadData() {
    state.loading = true
    state.error = null
    try {
      const result = await fetchFn({
        page: state.currentPage,
        pageSize
      })
      state.data = result.list
      state.total = result.total
    } catch (err) {
      state.error = err.message
      state.data = []
    } finally {
      state.loading = false
    }
  }

  async function nextPage() {
    if (hasNext.value) {
      state.currentPage++
      await loadData()
    }
  }

  async function prevPage() {
    if (hasPrev.value) {
      state.currentPage--
      await loadData()
    }
  }

  async function goToPage(page) {
    if (page >= 1 && page <= totalPages.value) {
      state.currentPage = page
      await loadData()
    }
  }

  return {
    state,
    totalPages,
    hasNext,
    hasPrev,
    loadData,
    nextPage,
    prevPage,
    goToPage
  }
}

在组件中使用:

<template>
  <div class="table-container">
    <el-table :data="state.data" v-loading="state.loading">
      <el-table-column prop="name" label="名称" />
      <el-table-column prop="status" label="状态" />
    </el-table>

    <el-pagination
      v-model:current-page="state.currentPage"
      :page-size="20"
      :total="state.total"
      @current-change="goToPage"
    />
  </div>
</template>

<script setup>
import { onMounted } from 'vue'
import { usePagination } from '@/composables/usePagination'
import { fetchUserList } from '@/api/user'

const {
  state,
  loadData,
  goToPage
} = usePagination(fetchUserList, { pageSize: 20 })

onMounted(() => loadData())
</script>

跨端组件设计:响应式布局适配方案

Vue3配合CSS变量和useMediaQuery组合式函数实现跨端响应式布局:

// composables/useMediaQuery.js
import { ref, onMounted, onUnmounted } from 'vue'

export function useMediaQuery(query) {
  const matches = ref(false)
  let mediaQuery

  const update = (e) => {
    matches.value = e.matches
  }

  onMounted(() => {
    mediaQuery = window.matchMedia(query)
    matches.value = mediaQuery.matches
    mediaQuery.addEventListener('change', update)
  })

  onUnmounted(() => {
    mediaQuery?.removeEventListener('change', update)
  })

  return matches
}

// 使用示例
const isMobile = useMediaQuery('(max-width: 768px)')
const isTablet = useMediaQuery('(max-width: 1024px)')

组件中根据断点动态渲染不同布局:

<template>
  <div :class="['layout', { 'layout--mobile': isMobile }]">
    <sidebar v-if="!isMobile" />
    <main-content />
    <mobile-nav v-if="isMobile" />
  </div>
</template>

watchEffect与watch的使用场景区分

watchEffect自动收集依赖并立即执行,适合副作用与多个响应式数据关联的场景。watch需要明确指定监听源,可以访问旧值,适合需要精确控制执行时机的场景。

// watchEffect:自动追踪依赖
watchEffect(() => {
  console.log(state.currentPage, state.pageSize)
  // 当currentPage或pageSize变化时自动重新执行
})

// watch:精确控制,可获取旧值
watch(
  () => state.currentPage,
  (newPage, oldPage) => {
    if (newPage !== oldPage) {
      loadData()
    }
  },
  { flush: 'post' }  // DOM更新后执行
)

flush选项设为post确保在DOM更新后执行回调,避免操作到未更新的DOM节点。immediate选项设为true可以在初始化时立即执行一次。

性能优化:shallowRef与markRaw的应用

处理大列表或不需要深度响应式的数据时,shallowRef和markRaw能减少不必要的代理开销:

import { shallowRef, markRaw } from 'vue'

// 大列表数据使用shallowRef,只对.value赋值触发更新
const bigList = shallowRef([])
bigList.value = await fetchHugeDataset()

// 第三方类实例使用markRaw跳过响应式代理
const chartInstance = markRaw(echarts.init(domEl))

前端工程化实践中,合理使用这些API可以显著降低内存占用和渲染开销,尤其在数据量大的管理后台场景中效果明显。

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

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

相关推荐