Vue3组合式API工程化实践与响应式性能优化

Vue3组合式API的响应式原理

Vue3的组合式API(Composition API)改变了组件逻辑的组织方式。核心响应式系统基于Proxy实现,相比Vue2的Object.defineProperty,Proxy能拦截更多操作类型(属性新增、删除、数组索引修改等),性能也更好。

响应式转换的基础API:

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

// ref用于基本类型
const count = ref(0)
// ref在template中自动解包,在JS中需要.value
console.log(count.value) // 0
count.value++

// reactive用于对象/数组
const state = reactive({
  user: { name: '张三', age: 28 },
  items: []
})

// 直接修改属性触发响应式更新
state.user.name = '李四'

// computed缓存计算结果
const fullName = computed(() => `${state.user.name}(${state.user.age})`)
// 只有依赖的响应式数据变化时才重新计算

// watch监听特定数据
watch(() => state.user.age, (newVal, oldVal) => {
  console.log(`年龄从${oldVal}变为${newVal}`)
}, { immediate: false, deep: false })

ref与reactive的选择策略

实际开发中ref和reactive的选择有明确规则。基本类型用ref,对象类型reactive和ref都可以,但需要注意各自的行为差异。

reactive的局限性:解构会丢失响应性,重新赋值会断开代理。这两个陷阱导致大量bug:

const state = reactive({ count: 0, name: 'test' })

// 问题1:解构丢失响应性
let { count, name } = state  // count和name变成普通变量
count++  // 不会触发更新

// 解决:用toRefs保持响应性
const { count, name } = toRefs(state)

// 问题2:整个替换丢失代理
state = reactive({ count: 1 })  // 模板中引用的是旧proxy

// 解决:用ref包裹对象
const state = ref({ count: 0 })
state.value = { count: 1 }  // ref本身不变,触发更新

推荐实践:数据层用reactive管理复杂状态,UI层用ref处理独立状态。表单数据等需要整体替换的场景统一用ref。

setup语法糖与组件通信

Vue3的<script setup>语法糖大幅简化了组件编写。父子组件通信的标准模式:

// 父组件 Parent.vue
<template>
  <ChildComp 
    :title="pageTitle"
    @update="handleUpdate"
    v-model:visible="dialogVisible" />
</template>

<script setup>
import { ref } from 'vue'
import ChildComp from './ChildComp.vue'

const pageTitle = ref('用户管理')
const dialogVisible = ref(false)

const handleUpdate = (data) => {
  console.log('收到子组件数据:', data)
}
</script>

// 子组件 ChildComp.vue
<template>
  <div>
    <h3>{{ title }}</h3>
    <button @click="emitUpdate">提交</button>
    <button @click="closeDialog">关闭</button>
  </div>
</template>

<script setup>
// defineProps和defineEmits是编译宏,无需导入
const props = defineProps({
  title: { type: String, required: true }
})

const emit = defineEmits(['update', 'update:visible'])

const emitUpdate = () => {
  emit('update', { id: 1, action: 'save' })
}

const closeDialog = () => {
  emit('update:visible', false)  // v-model双向绑定
}
</script>

跨层级组件通信用provide/inject,替代event bus:

// 顶层组件
<script setup>
import { provide, ref, readonly } from 'vue'

const theme = ref('dark')
const toggleTheme = () => {
  theme.value = theme.value === 'dark' ? 'light' : 'dark'
}
// readonly防止子组件直接修改
provide('theme', readonly(theme))
provide('toggleTheme', toggleTheme)
</script>

// 任意后代组件
<script setup>
import { inject } from 'vue'

const theme = inject('theme')        // Ref<string>
const toggleTheme = inject('toggleTheme')  // Function
</script>

响应式性能优化技巧

大型列表渲染和频繁更新的场景需要针对性优化。关键手段包括shallowRef、markRaw、虚拟列表。

shallowRef只追踪.value的变化,不深度代理内部属性,适合大对象:

import { shallowRef, watchEffect } from 'vue'

// 普通ref会深度代理10000个对象
// const list = ref([...Array(10000)])  // 性能差

// shallowRef只追踪引用变化
const list = shallowRef([])
// 赋值新数组触发更新
list.value = Array.from({ length: 10000 }, (_, i) => ({ id: i }))
// 修改内部属性不触发更新
list.value[0].id = 999  // 不触发
list.value = [...list.value]  // 触发

markRaw标记对象为永不响应,跳过Proxy代理,适合第三方实例:

import { markRaw, reactive } from 'vue'
import mapboxgl from 'mapbox-gl'

const state = reactive({
  // 地图实例不需要响应式追踪
  mapInstance: markRaw(new mapboxgl.Map({ container: 'map' })),
  // Chart实例同理
  chart: markRaw(echarts.init(document.getElementById('chart')))
})

虚拟列表处理万级数据渲染,只渲染可视区域:

// @vueuse/core的useVirtualList
import { useVirtualList } from '@vueuse/core'

const { list, containerProps, wrapperProps } = useVirtualList(
  largeData,        // 原始数据
  { itemHeight: 60 } // 每项固定高度
)

// template中使用
// <div v-bind="containerProps" style="height: 500px; overflow: auto;">
//   <div v-bind="wrapperProps">
//     <div v-for="item in list" :key="item.data.id" style="height: 60px;">
//       {{ item.data.name }}
//     </div>
//   </div>
// </div>

computed与watch的正确用法

computed用于派生状态,自动缓存依赖结果。watch用于副作用执行。两者的正确使用场景需要区分:

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

const price = ref(100)
const quantity = ref(2)

// computed:派生新值,有缓存
// 适合模板中使用的计算属性
const totalPrice = computed(() => price.value * quantity.value)
// price和quantity都没变时,多次访问totalPrice.value只计算一次

// watch:显式监听变化,执行副作用
// 适合发请求、写日志等操作
watch(totalPrice, (newVal) => {
  // totalPrice变化时执行
  fetchShippingFee(newVal)
}, { flush: 'post' })  // DOM更新后执行

// watchEffect:自动收集依赖,立即执行
// 适合依赖关系复杂、无法预先确定的场景
watchEffect(() => {
  // 自动追踪内部访问的所有响应式数据
  document.title = `总价: ${price.value * quantity.value}元`
})

// watch监听多个源
watch([price, quantity], ([newPrice, newQuantity]) => {
  console.log(`价格${newPrice},数量${newQuantity}`)
})

// watch深度监听对象
const config = ref({ api: { url: '/api', timeout: 5000 } })
watch(config, (newVal) => {
  console.log('config changed', newVal)
}, { deep: true })  // deep选项开启深度监听

性能原则:能computed解决的不要用watch,computed的缓存机制天然优化了重复计算。watch加deep: true会遍历整个对象树,大对象场景下有明显性能开销。

自定义组合式函数封装

组合式函数(Composables)是Vue3复用逻辑的核心模式,替代Vue2的mixin。命名规范为use开头:

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

export function usePagination(fetchFn, options = {}) {
  const currentPage = ref(options.defaultPage || 1)
  const pageSize = ref(options.pageSize || 10)
  const total = ref(0)
  const loading = ref(false)
  const data = ref([])

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

  const fetchData = async () => {
    loading.value = true
    try {
      const res = await fetchFn({
        page: currentPage.value,
        size: pageSize.value
      })
      data.value = res.list
      total.value = res.total
    } finally {
      loading.value = false
    }
  }

  const changePage = (page) => {
    currentPage.value = page
    fetchData()
  }

  // 组件卸载时清理
  onUnmounted(() => {
    data.value = []
  })

  return {
    currentPage,
    pageSize,
    total,
    totalPages,
    loading,
    data,
    fetchData,
    changePage
  }
}

// 使用
const { currentPage, data, loading, changePage, fetchData } = 
  usePagination(api.getUserList, { pageSize: 20 })

组合式函数的优势在于状态隔离——每个组件调用usePagination时创建独立的响应式状态,不存在mixin的命名冲突问题。多个composable组合使用,构建复杂业务逻辑时保持代码清晰可维护。

原创文章,作者:小编,如若转载,请注明出处:https://www.yunthe.com/vue3-zu-he-shi-api-gong-cheng-hua-shi-jian-yu-xiang-ying/

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

相关推荐