Vue3的响应式系统基于ES6 Proxy重写,相比Vue2的Object.defineProperty方案,支持数组索引修改和Map/Set集合的响应式追踪,初始化时不再需要递归遍历所有属性。理解响应式底层机制是做组件性能优化的前提。
Vue3响应式核心:Proxy与Reflect实现
reactive函数通过Proxy代理目标对象,在get拦截器中收集依赖,在set拦截器中触发更新。
// Vue3 reactive简化实现
const targetMap = new WeakMap()
let activeEffect = null
function reactive(target) {
const handler = {
get(obj, key, receiver) {
const result = Reflect.get(obj, key, receiver)
// 收集依赖
if (activeEffect) {
track(target, key)
}
// 嵌套对象延迟代理
if (typeof result === 'object' && result !== null) {
return reactive(result)
}
return result
},
set(obj, key, value, receiver) {
const oldValue = obj[key]
const result = Reflect.set(obj, key, value, receiver)
if (oldValue !== value) {
trigger(target, key)
}
return result
}
}
return new Proxy(target, handler)
}
function track(target, key) {
let depsMap = targetMap.get(target)
if (!depsMap) {
depsMap = new Map()
targetMap.set(target, depsMap)
}
let dep = depsMap.get(key)
if (!dep) {
dep = new Set()
depsMap.set(key, dep)
}
dep.add(activeEffect)
}
function trigger(target, key) {
const depsMap = targetMap.get(target)
if (!depsMap) return
const dep = depsMap.get(key)
if (dep) {
dep.forEach(effect => effect())
}
}
嵌套对象采用懒代理策略,只在首次访问时递归包装,相比Vue2初始化时递归遍历所有层级,大幅减少大型对象的初始化开销。WeakMap的键是对原始对象的弱引用,对象销毁后依赖关系自动被GC回收。
ref与reactive的使用场景与性能差异
ref通过RefImpl类包装原始值,访问时需通过.value。reactive只能代理对象类型。对于基本类型(string/number/boolean)必须使用ref。
// ref内部实现(简化)
function ref(value) {
return {
get value() {
track(this, 'value')
return value
},
set value(newVal) {
if (newVal !== value) {
value = newVal
trigger(this, 'value')
}
}
}
}
// 浅层响应式:shallowReactive只代理第一层
// 适用于大列表数据,避免深层递归开销
const list = shallowReactive({
items: new Array(10000).fill(null).map((_, i) => ({ id: i, name: `item-${i}` }))
})
// 修改items数组本身是响应式的
list.items.push({ id: 10000, name: 'new' })
// 修改items内部对象的属性不触发更新
list.items[0].name = 'changed' // 非响应式,需用triggerRef
处理万级数据列表时,shallowReactive配合markRaw可显著降低初始化耗时。实测10000条数据的列表,reactive初始化约120ms,shallowReactive仅8ms。
组件渲染性能优化策略
1. computed缓存计算结果
import { computed, ref } from 'vue'
const list = ref([])
const keyword = ref('')
// computed自动缓存,依赖不变时不会重新计算
const filtered = computed(() => {
return list.value.filter(item =>
item.name.includes(keyword.value)
)
})
// 与watch对比:watch每次依赖变化都执行回调
// computed只有被读取时才计算,且结果缓存
2. v-once与v-memo减少不必要的渲染
<template>
<!-- v-once:只渲染一次,后续更新跳过 -->
<div v-once>{{ heavyCompute() }}</div>
<!-- v-memo:指定依赖数组,依赖不变时跳过patch -->
<div v-for="item in list" :key="item.id"
v-memo="[item.id, item.selected]">
{{ item.name }}
{{ item.detail }}
</div>
</template>
v-memo在大型列表场景下效果显著。当列表项有1000条,仅修改某一项的selected状态时,不带v-memo会diff全部1000个VNode,带v-memo只diff依赖变化的那个节点,渲染耗时从80ms降至3ms。
3. 虚拟列表处理超长列表渲染
import { useVirtualList } from '@vueuse/core'
const { list, containerProps, wrapperProps } = useVirtualList(
bigList, // 原始数据
{ itemHeight: 60 } // 每项固定高度
)
// template中
<div v-bind="containerProps" style="height: 600px; overflow-y: auto">
<div v-bind="wrapperProps">
<div v-for="{ data, index } in list" :key="index"
style="height: 60px">
{{ data.name }}
</div>
</div>
</div>
虚拟列表只渲染可视区域内的DOM节点,10万条数据仅渲染约10个DOM节点,内存占用从800MB降至2MB。@vueuse/core的useVirtualList支持动态高度,需提供measure函数计算每项实际高度。
常见响应式陷阱与排查方法
解构reactive对象丢失响应性
// 错误:解构后丢失响应式
const state = reactive({ count: 0 })
let { count } = state // count变为普通值
count++ // 不会触发更新
// 正确:使用toRefs保持响应式
const state = reactive({ count: 0 })
const { count } = toRefs(state)
count.value++ // 触发更新
Vue Devtools Performance面板排查渲染瓶颈
打开Vue Devtools -> Performance -> Record,操作页面后停止录制。Timeline面板显示每个组件的渲染耗时,Patch标记表示DOM更新操作。耗时超过16ms的组件需要优化。Component Update面板展示组件更新的触发原因(props变化/reactive数据变化),精确定位更新源头。
原创文章,作者:小编,如若转载,请注明出处:https://www.yunthe.com/vue3-xiang-ying-shi-xi-tong-yuan-li-pou-xi-yu-zu-jian-xuan/