从 Vue 2 到 Vue 3:响应式的演进
Vue 2 的响应式系统基于 Object.defineProperty,通过为对象的每个属性设置 getter 和 setter 来追踪依赖和触发更新。这种方案存在几个显著的局限性:无法检测属性的新增和删除、无法直接监听数组下标的变化、深层嵌套对象需要递归遍历导致性能开销较大。
Vue 3 全面转向了 ES6 的 Proxy,从根本上解决了上述问题。Proxy 可以拦截对象上的几乎所有操作,包括属性读写、属性删除、in 操作符、for...in 循环等,使得响应式覆盖面更加完整。
// Vue 2 的 defineProperty 方案(简化版)
function defineReactive(obj, key, val) {
const dep = new Dep()
Object.defineProperty(obj, key, {
get() {
if (Dep.target) {
dep.depend()
}
return val
},
set(newVal) {
if (newVal === val) return
val = newVal
dep.notify()
}
})
}
// Vue 3 的 Proxy 方案(简化版)
function reactive(target) {
return new Proxy(target, {
get(obj, key, receiver) {
track(obj, key)
return Reflect.get(obj, key, receiver)
},
set(obj, key, value, receiver) {
const result = Reflect.set(obj, key, value, receiver)
trigger(obj, key)
return result
},
deleteProperty(obj, key) {
const result = Reflect.deleteProperty(obj, key)
trigger(obj, key)
return result
}
})
}
Proxy 的核心机制
Proxy 对象接受两个参数:目标对象和处理器对象(handler)。处理器中可以定义各种拦截行为(trap),Vue 3 主要使用了 get、set、has、deleteProperty、ownKeys 等 trap。配合 Reflect API 执行默认行为,保证了拦截逻辑与原始语义的一致性。
值得注意的是,Proxy 的拦截是惰性的。只有当嵌套对象被实际访问时,才会递归地对子对象进行 Proxy 包装,这比 Vue 2 在初始化时一次性递归遍历整个对象树要高效得多。
reactive() 与 ref()
Vue 3 提供了两种声明响应式状态的 API。reactive() 接受一个对象并返回其 Proxy 代理,适合管理复杂的对象状态。ref() 则可以接受任意类型的值,内部通过一个带有 __v_isRef 标记的对象进行包装,在模板中自动解包。
import { reactive, ref } from 'vue'
// reactive 适用于对象类型
const state = reactive({
count: 0,
user: { name: '张三', age: 25 }
})
state.count++ // 直接访问,触发响应
// ref 适用于基本类型或需要替换整个值的场景
const count = ref(0)
const list = ref([1, 2, 3])
count.value++ // JS 中需要 .value
list.value = [4, 5, 6] // 可以整体替换
// 模板中 ref 自动解包,无需 .value
// <template>
// <p>{{ count }}</p>
// </template>
依赖收集与触发
Vue 3 的依赖收集系统基于 effect 函数。当组件的渲染函数执行时,会建立一个渲染 effect。在读取响应式属性时,get trap 中的 track() 函数会将当前正在执行的 effect 记录为该属性的依赖。当属性值发生变化时,set trap 中的 trigger() 函数会通知所有依赖重新执行。
// 简化的依赖收集实现
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) {
dep.forEach(effect => effect.scheduler ? effect.scheduler() : effect.run())
}
}
// effect 执行
function effect(fn) {
const _effect = {
run() {
activeEffect = _effect
const result = fn()
activeEffect = null
return result
}
}
_effect.run()
return _effect
}
computed 的实现原理
computed 内部也是一个 effect,但它具有惰性求值和缓存机制。只有当依赖变化时才会标记为 dirty,在被访问时才重新计算。这种脏标记策略避免了不必要的重复计算。
function computed(getter) {
let value
let dirty = true
const _effect = effect(getter, {
scheduler() {
dirty = true // 依赖变化时标记为脏
trigger(obj, 'value') // 通知使用该 computed 的依赖
}
})
const obj = {
get value() {
if (dirty) {
value = _effect.run()
dirty = false
}
track(obj, 'value')
return value
}
}
return obj
}
shallowReactive 与性能优化
shallowReactive() 只对对象的第一层属性进行响应式代理,嵌套对象不会被递归包装。在处理大型数据结构时,如果只有顶层属性需要响应式追踪,使用 shallowReactive 可以显著减少 Proxy 的创建数量和内存占用。
import { shallowReactive, reactive } from 'vue'
// shallowReactive:仅顶层响应
const shallow = shallowReactive({
foo: 1,
nested: { bar: 2 }
})
shallow.foo++ // 触发更新 ✓
shallow.nested.bar = 99 // 不触发更新 ✗
// reactive:深层响应
const deep = reactive({
foo: 1,
nested: { bar: 2 }
})
deep.nested.bar = 99 // 触发更新 ✓
类似地,shallowRef() 只追踪 .value 本身的变化,不会对值内部的属性做响应式转换。markRaw() 则可以标记一个对象永远不被转为响应式,适用于嵌入不可变第三方库实例的场景。合理选用这些 API,能在保持响应式能力的同时优化运行时性能。