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/