Vue3的响应式系统是整个框架的核心。相比Vue2基于Object.defineProperty的实现,Vue3改用ES6 Proxy重写了响应式引擎,解决了数组监听、属性新增删除、深层嵌套等历史问题。理解响应式原理对排查响应式失效、优化渲染性能至关重要。
从Object.defineProperty到Proxy的演进
Vue2的响应式通过Object.defineProperty递归遍历data对象的每个属性,为每个属性设置getter和setter。这个方案有三个硬伤:无法检测属性的新增和删除(需要Vue.set/Vue.delete);无法直接监听数组索引修改和length变化(通过重写数组原型方法绕过);初始化时递归遍历所有属性,深层对象初始化开销大。
Vue3用Proxy替代了Object.defineProperty。Proxy拦截整个对象而非单个属性,天然支持属性增删检测和数组操作监听。且Proxy是惰性的——只有访问到某个属性时才递归代理其子对象,初始化时只创建一层代理。
reactive函数源码级解析
reactive是Vue3最基础的响应式API。它的核心是createReactiveObject函数,通过Proxy的handler实现依赖收集和触发更新。
// 简化的reactive实现
const reactiveMap = new WeakMap()
function reactive(target) {
if (target.__v_isReactive) return target
const existingProxy = reactiveMap.get(target)
if (existingProxy) return existingProxy
const proxy = new Proxy(target, {
get(target, key, receiver) {
if (key === '__v_isReactive') return true
const result = Reflect.get(target, key, receiver)
// 依赖收集:记录"谁在读取这个属性"
track(target, key)
// 惰性递归:只有访问时才将子对象转为响应式
if (typeof result === 'object' && result !== null) {
return reactive(result)
}
return result
},
set(target, key, value, receiver) {
const oldValue = target[key]
const result = Reflect.set(target, key, value, receiver)
// 只有值真正变化才触发更新
if (!Object.is(oldValue, value)) {
trigger(target, key)
}
return result
},
deleteProperty(target, key) {
const result = Reflect.deleteProperty(target, key)
trigger(target, key)
return result
}
})
reactiveMap.set(target, proxy)
return proxy
}
track函数在getter中执行,将当前正在执行的副作用函数(effect)与被读取的属性建立映射关系。这个映射存储在全局的targetMap中,结构为WeakMap -> Map -> Set:
// 依赖收集的数据结构
let activeEffect = null
const targetMap = new WeakMap()
function track(target, key) {
if (!activeEffect) return
let depsMap = targetMap.get(target)
if (!depsMap) {
targetMap.set(target, (depsMap = new Map()))
}
let dep = depsMap.get(key)
if (!dep) {
depsMap.set(key, (dep = new Set()))
}
dep.add(activeEffect)
}
function trigger(target, key) {
const depsMap = targetMap.get(target)
if (!depsMap) return
const dep = depsMap.get(key)
if (dep) {
// 拷贝后遍历,防止遍历过程中Set被修改
[...dep].forEach(effect => effect())
}
}
ref与reactive的区别与选择
reactive只能代理对象类型(Object、Array、Map、Set),对于基本类型(string、number、boolean)无能为力。ref通过对象包装的方式解决这个问题——将基本类型值存放在.value属性上,对.value做依赖收集。
function ref(value) {
return {
get value() {
track(this, 'value')
return value
},
set value(newVal) {
if (!Object.is(value, newVal)) {
value = newVal
trigger(this, 'value')
}
}
}
}
实际使用中,ref和reactive的选择遵循以下原则:基本类型用ref,对象类型看场景。如果整个对象会被整体替换(如API返回后重新赋值),用ref更方便,因为reactive返回的代理对象不能直接替换。如果对象内容逐步修改,reactive语法更简洁。
// ref方案:对象整体替换
const user = ref({ name: '张三', age: 25 })
user.value = await fetchUser() // 整体替换,响应式正常
// reactive方案:逐步修改
const state = reactive({ user: null, loading: false })
state.loading = true
state.user = await fetchUser() // 修改属性,响应式正常
state = { user: null, loading: false } // 错误!直接替换失去响应式
computed与watch的实现原理
computed本质上是一个带缓存的响应式依赖。它内部维护一个dirty标志,只有当依赖的响应式数据变化时才将dirty设为true,下次访问时才重新计算。
function computed(getter) {
let value
let dirty = true
const effect = createReactiveEffect(() => {
value = getter()
dirty = false
})
effect.run() // 首次执行,建立依赖关系
return {
get value() {
if (dirty) {
effect.run()
}
track(this, 'value')
return value
}
}
}
watch的实现基于effect和onCleanup回调。watch监听一个getter函数或ref,在其值变化时执行回调函数。关键在于新旧值的比较逻辑和副作用清理机制。
// watch的基本用法
const count = ref(0)
watch(count, (newVal, oldVal, onCleanup) => {
const timer = setTimeout(() => {
console.log('debounced:', newVal)
}, 300)
// 清理上一次的副作用
onCleanup(() => clearTimeout(timer))
})
// watchEffect自动收集依赖
watchEffect((onCleanup) => {
// 自动追踪内部用到的所有响应式数据
console.log('count changed:', count.value)
})
响应式性能优化技巧
大型应用中,过度的响应式追踪会导致性能问题。以下优化手段值得注意:
shallowRef/shallowReactive:只对第一层做响应式,深层访问不递归代理,适合大对象列表的场景。
const largeList = shallowRef([])
// 只追踪.value的赋值,内部数组修改需手动触发
largeList.value = newArray // 响应式触发
largeList.value.push(item) // 不会触发更新,需要替换整个数组
// 使用triggerRef手动触发
largeList.value.push(item)
triggerRef(largeList) // 手动通知更新
markRaw:标记对象永远不被转为响应式,适合第三方实例、Canvas对象等不需要响应式的数据。
const chartInstance = markRaw(echarts.init(dom))
// echarts实例不会被Proxy包装,避免性能浪费和潜在bug
v-once和v-memo:v-once让元素只渲染一次,后续更新跳过。v-memo根据条件缓存子树,只有依赖数组变化时才重新渲染。
<!-- 只渲染一次,后续更新不参与diff -->
<div v-once>{{ heavyCompute() }}</div>
<!-- 只有item.id或item.selected变化时才重新渲染 -->
<div v-for="item in list" :key="item.id" v-memo="[item.id, item.selected]">
<span>{{ item.name }}</span>
<span>{{ item.detail }}</span>
</div>
调试响应式问题时,Vue Devtools的Components面板可以查看组件的响应式数据结构和依赖关系。Vue3还提供了toRaw函数获取代理对象的原始对象,用于判断响应式状态。
原创文章,作者:小编,如若转载,请注明出处:https://www.yunthe.com/vue3-xiang-ying-shi-yuan-li-shen-du-jie-xi-proxy-yu-ref-de/