主题
深入响应式
Vue 中的响应性是如何工作的
在 JavaScript 中有两种劫持 property 访问的方式:getter / setters 和 Proxy。
- Vue 2 使用
getter / setters
完全是出于支持旧版本浏览器的限制。 - Vue 3 中则使用了
Proxy
来创建响应式对象,仅将 getter / setter 用于 ref。
伪代码:
js
function reactive(obj) {
return new Proxy(obj, {
get(target, key) {
track(target, key)
return target[key]
},
set(target, key, value) {
target[key] = value
trigger(target, key)
},
})
}
function ref(value) {
const refObject = {
get value() {
track(refObject, 'value')
return value
},
set value(newValue) {
value = newValue
trigger(refObject, 'value')
},
}
return refObject
}
依赖收集机制
依赖收集(Dependency Collection)是 Vue 响应式系统的核心机制。
Vue 2 依赖收集
Vue 2 依赖收集 通过 Object.defineProperty
劫持属性,基于 Dep
和 Watcher
进行依赖管理,但存在新增属性、数组索引、深层对象性能等问题。
基于 Object.defineProperty
Vue 2 使用 Object.defineProperty()
劫持数据对象的 getter 和 setter,在访问属性时收集依赖,在修改属性时触发更新。
依赖收集流程
关键角色
Dep
(依赖管理类):存储所有订阅当前属性的Watcher
实例,并在数据变更时通知它们更新。Watcher
(观察者):表示组件或计算属性,每个Watcher
订阅多个Dep
,当Dep
触发更新时,Watcher
重新计算或触发渲染。
依赖收集过程
访问数据时(getter 触发)
getter
触发时,当前正在执行的Watcher
(例如组件渲染)会被存入Dep
。Dep
将Watcher
存入subs
(订阅者列表)。
数据更新时(setter 触发)
setter
修改数据时,会通知Dep
,触发subs
中的所有Watcher
进行更新。
示例代码
js
class Dep {
constructor() {
this.subs = []
}
depend() {
if (Dep.target) {
this.subs.push(Dep.target) // 依赖收集
}
}
notify() {
this.subs.forEach(watcher => watcher.update()) // 触发更新
}
}
Dep.target = null // 当前 Watcher
function defineReactive(obj, key, val) {
const dep = new Dep()
Object.defineProperty(obj, key, {
get() {
dep.depend() // 依赖收集
return val
},
set(newVal) {
val = newVal
dep.notify() // 触发更新
},
})
}
const data = {}
defineReactive(data, 'msg', 'hello')
function updateComponent() {
console.log('组件更新', data.msg)
}
// 创建 Watcher
Dep.target = { update: updateComponent }
console.log(data.msg) // 触发 getter,收集依赖
Dep.target = null // 依赖收集结束
data.msg = 'world' // 触发 setter,通知 Watcher 更新
✅ Vue 2 依赖收集基于 Object.defineProperty
,但它有一些缺陷:
- 无法监听数组索引的变化(只能监听
push
、pop
等变更方法)。 - 无法动态添加新属性(新增属性不是响应式的,需要
Vue.set()
)。 - 深层嵌套对象依赖收集成本高(需要递归遍历整个对象)。
Vue 3 依赖收集
Vue 3 依赖收集 基于 Proxy
实现 track()
/trigger()
,更灵活,性能更优,能自动追踪属性的新增和删除,适用于复杂的响应式需求。
基于 Proxy
Vue 3 通过 Proxy
代理整个对象,不再使用 Object.defineProperty
,可以直接监听新增属性和删除属性的变化。
依赖收集流程
关键角色
ReactiveEffect
(响应式副作用):类似 Vue 2 的Watcher
,负责执行副作用(组件渲染、计算属性等)。targetMap
(存储依赖的全局 Map):targetMap
维护所有响应式对象的依赖信息。targetMap.get(obj)
得到obj
的depsMap
(存储对象obj
内各个属性的ReactiveEffect
)。depsMap.get(key)
得到key
具体的Set<ReactiveEffect>
,即依赖于key
的所有ReactiveEffect
。
依赖收集过程
访问数据时(getter 触发)
activeEffect
是当前正在运行的副作用(如setup()
或computed
)。track(target, key)
记录activeEffect
到targetMap
,实现依赖收集。
数据更新时(setter 触发)
trigger(target, key)
遍历targetMap[target][key]
中的ReactiveEffect
,触发更新。
示例代码
js
const targetMap = new WeakMap()
let activeEffect = null
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())
}
}
// 代理对象
function reactive(obj) {
return new Proxy(obj, {
get(target, key, receiver) {
track(target, key)
return Reflect.get(target, key, receiver)
},
set(target, key, value, receiver) {
const result = Reflect.set(target, key, value, receiver)
trigger(target, key)
return result
},
})
}
// 测试
const state = reactive({ count: 0 })
function effect(fn) {
activeEffect = fn
fn()
activeEffect = null
}
effect(() => {
console.log('组件更新', state.count)
})
state.count = 1 // 触发更新
✅ Vue 3 依赖收集基于 Proxy
,相比 Vue 2 具有明显优势:
- 支持新增/删除属性,不需要
Vue.set()
。 - 对数组的索引变更具有响应性。
- 优化了性能,避免 Vue 2 递归劫持所有属性的开销。
Vue 2 vs Vue 3 依赖收集对比总结
机制 | Vue 2 | Vue 3 |
---|---|---|
响应式实现 | Object.defineProperty | Proxy |
依赖管理 | Dep / Watcher | ReactiveEffect / targetMap |
依赖收集 | getter 触发 dep.depend() | getter 触发 track() |
触发更新 | setter 触发 dep.notify() | setter 触发 trigger() |
新增属性 | 需要 Vue.set() | 自动追踪 |
删除属性 | 不能追踪 | 自动追踪 |
数组索引 | 不能追踪 | 可以追踪 |
深度监听 | 递归遍历所有属性 | 访问时动态代理 |