Vue2 Object.defineProperty 响应式原理
Vue2 如何通过 Object.defineProperty 实现响应式,以及其局限。
#type / concept
#status / growing
#tech / dev / frame
#resource / vue2
[!info] related notes
- 所属 MOC: Vue MOC
- 前置概念: [[ecmascript-object-defineproperty|Object.defineProperty]]
- 并列概念: Vue3 Proxy 响应式原理, Vue2 与 Vue3 响应式系统对比
- 易混淆概念: Vue3 用 Proxy,Vue2 用 Object.defineProperty
- 关系笔记: Vue2 与 Vue3 响应式系统对比
Vue2 Object.defineProperty 响应式原理
一句话定义
Vue2 在初始化阶段递归遍历 data 对象每个属性,用 Object.defineProperty 把它们改造成 getter/setter,在 getter 中收集依赖、setter 中触发更新。
核心内容
依赖收集:Dep + Watcher
Vue2 的依赖模型由三个核心角色组成:
- Dep:每个响应式属性的”订阅者容器”,负责收集和通知 Watcher
- Watcher:需要在数据变化时重新执行的任务(组件渲染、computed、watch)
- Dep.target:全局指针,指向当前正在执行的 Watcher
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()
}
})
}
初始化流程
function observe(data) {
if (!isObject(data)) return
Object.keys(data).forEach(key => {
defineReactive(data, key, data[key])
if (isObject(data[key])) {
observe(data[key]) // 递归处理深层对象
}
})
}
特点:初始化时深度遍历 + 属性劫持
数组的特殊处理
Object.defineProperty 无法拦截数组索引赋值和 length 修改,所以 Vue2 重写了数组的变异方法:
const proto = Array.prototype
const arrayMethods = Object.create(proto)
;['push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse'].forEach(method => {
const original = proto[method]
arrayMethods[method] = function(...args) {
const result = original.apply(this, args)
dep.notify() // 通知更新
return result
}
})
边界与易混淆点
Vue2 响应式的局限
| 场景 | Vue2 表现 |
|---|---|
| 新增属性 | 不会响应,需用 Vue.set |
| 删除属性 | 不会响应,需用 Vue.delete |
arr[index] = value | 不触发更新 |
arr.length = 0 | 不触发更新 |
| Map / Set | 支持差 |
为什么要用 Vue.set / Vue.delete
因为新增/删除属性没有对应的 getter/setter,Vue2 无法拦截这些操作。Vue.set 的实现原理:
Vue.set(target, key, value) {
defineReactive(target, key, value)
target.__ob__.dep.notify() // 触发更新
}
和 Vue3 Proxy 的本质区别
- Vue2:预先劫持每个属性,初始化成本高
- Vue3:运行时代理整个对象,惰性初始化