Vue
响应式原理
一、Vue 2 — Object.defineProperty
核心:遍历对象每个 key,用 defineProperty 劫持 getter / setter。
function defineReactive(obj, key, val) {
const dep = new Dep() // 每个 key 一个依赖收集器
Object.defineProperty(obj, key, {
get() {
if (Dep.target) dep.depend() // 收集依赖(谁在用我)
return val
},
set(newVal) {
if (newVal === val) return
val = newVal
dep.notify() // 通知更新(告诉用我的人:我变了)
},
})
}
// 初始化时递归遍历所有 key
// Object.keys 只返回可枚举属性,不可枚举的 key 不会被劫持
function observe(data) {
Object.keys(data).forEach((key) => {
defineReactive(data, key, data[key])
})
}缺陷:
// 新增属性 → 检测不到(初始化时没有这个 key,没挂 getter/setter)
this.obj.newKey = 'hello' // ❌ 不触发更新
// 删除属性 → 检测不到
delete this.obj.name // ❌ 不触发更新
// 数组下标直接赋值 → 检测不到
this.arr[0] = 'new' // ❌ 不触发更新
// 修改数组长度 → 检测不到
this.arr.length = 0 // ❌ 不触发更新二、$set 解决了什么
专门解决"新增属性检测不到"和"数组下标赋值检测不到"两个问题。
this.$set(this.obj, 'newKey', 'hello') // ✅ 触发更新
this.$set(this.arr, 0, 'new') // ✅ 触发更新内部实现(简化):
function set(target, key, val) {
// 数组:走 splice(Vue 重写过 splice,能触发更新)
if (Array.isArray(target)) {
target.splice(key, 1, val)
return val
}
// 对象已有的 key:已经有 getter/setter,直接赋值
if (key in target) {
target[key] = val
return val
}
// 对象新增 key:手动补上 defineReactive,再手动通知更新
defineReactive(target, key, val)
target.__ob__.dep.notify()
return val
}本质:手动给新属性补 defineProperty,再手动通知依赖。
三、Vue 3 — Proxy
核心:用 Proxy 代理整个对象,不再逐个 key 劫持。
function reactive(target) {
return new Proxy(target, {
get(target, key, receiver) {
track(target, key) // 收集依赖
const result = Reflect.get(target, key, receiver)
if (isObject(result)) return reactive(result) // 惰性递归
return result
},
set(target, key, value, receiver) {
const result = Reflect.set(target, key, value, receiver)
trigger(target, key) // 触发更新
return result
},
deleteProperty(target, key) {
const result = Reflect.deleteProperty(target, key)
trigger(target, key) // 删除也能拦截
return result
},
})
}不再需要 $set:
const state = reactive({ name: 'Tom' })
state.age = 18 // ✅ 新增属性,Proxy set 陷阱自动捕获
delete state.name // ✅ 删除属性,deleteProperty 陷阱自动捕获
const arr = reactive([1, 2, 3])
arr[0] = 99 // ✅ 触发更新
arr.length = 1 // ✅ 触发更新根本原因:Proxy 拦截的是对象级别的操作,新增、删除、任意 key 都经过代理,天然全覆盖。defineProperty 拦截的是已知 key 级别,初始化之后新增的 key 完全不感知。
四、对比总结
| Vue 2 | Vue 3 | |
|---|---|---|
| 底层 API | Object.defineProperty | Proxy |
| 劫持粒度 | 逐个 key | 整个对象 |
| 新增属性 | ❌ 需要 $set | ✅ 自动检测 |
| 删除属性 | ❌ 需要 $delete | ✅ 自动检测 |
| 数组下标 | ❌ 需要 $set | ✅ 自动检测 |
| 初始化性能 | 递归遍历所有 key,性能差 | 惰性代理,访问时才递归,性能好 |
| 兼容性 | IE9+ | 不支持 IE(Proxy 无法 polyfill) |
五、一句话总结
Vue 2 用
defineProperty按 key 劫持,新增 / 删除属性拦截不到,需要$set手动补响应式。Vue 3 用Proxy代理整个对象,任何操作都能拦截,$set自然就不需要了。