响应式系统是现代前端框架的基石。Vue 2 依靠 Object.defineProperty 实现数据劫持,而 Vue 3 则全面转向 Proxy。本文将从零实现一个迷你响应式系统,并深入对比这两种方案的优劣。
一、响应式系统的核心目标
一个响应式系统需要完成三件事:
- 数据劫持:拦截对数据的读取和写入。
- 依赖收集:在读取数据时,记录“谁”用到了这个数据。
- 派发更新:在数据变化时,通知所有依赖它的“人”重新执行。
下面我们用两种方式分别实现一个最小可用的版本。
二、基于 Object.defineProperty 的实现
2.1 核心代码
function observe(obj) {
if (typeof obj !== 'object' || obj === null) return
Object.keys(obj).forEach(key => {
let value = obj[key]
const dep = []
Object.defineProperty(obj, key, {
get() {
if (window.__activeEffect) {
dep.push(window.__activeEffect)
}
return value
},
set(newVal) {
if (newVal === value) return
value = newVal
dep.forEach(fn => fn())
}
})
})
}
2.2 使用示例
const state = { count: 0 }
observe(state)
function effect(fn) {
window.__activeEffect = fn
fn()
window.__activeEffect = null
}
effect(() => {
console.log('count is:', state.count)
})
state.count++ // 输出: count is: 1
2.3 存在的问题
- 无法监听新增/删除属性:
state.newKey = 1不会触发更新,Vue 2 因此提供了Vue.set/Vue.delete。 - 无法监听数组索引和 length 变化:Vue 2 通过重写数组的 7 个方法(push、pop 等)来绕过。
- 初始化时需递归遍历所有属性:性能开销大,尤其是深层嵌套对象。
- 每个属性都需要创建一个闭包和 dep 数组:内存占用较高。
三、基于 Proxy 的实现
3.1 核心代码
function reactive(obj) {
const depMap = new Map()
return new Proxy(obj, {
get(target, key, receiver) {
const value = Reflect.get(target, key, receiver)
if (window.__activeEffect) {
if (!depMap.has(key)) depMap.set(key, [])
depMap.get(key).push(window.__activeEffect)
}
// 嵌套对象惰性代理
if (typeof value === 'object' && value !== null) {
return reactive(value)
}
return value
},
set(target, key, value, receiver) {
const oldVal = target[key]
const result = Reflect.set(target, key, value, receiver)
if (oldVal !== value && depMap.has(key)) {
depMap.get(key).forEach(fn => fn())
}
return result
},
deleteProperty(target, key) {
const hadKey = key in target
const result = Reflect.deleteProperty(target, key)
if (hadKey && depMap.has(key)) {
depMap.get(key).forEach(fn => fn())
}
return result
}
})
}
3.2 使用示例
const state = reactive({ count: 0 })
function effect(fn) {
window.__activeEffect = fn
fn()
window.__activeEffect = null
}
effect(() => {
console.log('count is:', state.count)
})
state.count++ // 输出: count is: 1
state.newKey = 'hi' // 新增属性也能触发(如果 effect 中读取过 newKey)
delete state.count // 删除属性也能触发
3.3 优势
- 支持新增/删除属性:通过
deleteProperty和set拦截。 - 支持数组索引和 length 变化:Proxy 天然支持。
- 惰性代理:只有访问到嵌套对象时才递归代理,初始化性能更好。
- 代码更简洁:不需要为每个属性单独定义 getter/setter。
四、两种方案对比
| 维度 | Object.defineProperty | Proxy |
|---|---|---|
| 监听新增属性 | ❌ 不支持 | ✅ 支持 |
| 监听删除属性 | ❌ 不支持 | ✅ 支持 |
| 监听数组索引 | ❌ 不支持 | ✅ 支持 |
| 监听数组 length | ❌ 不支持 | ✅ 支持 |
| 初始化性能 | 差(需递归遍历) | 好(惰性代理) |
| 内存占用 | 高(每个属性一个闭包) | 低 |
| 兼容性 | IE9+ | 不支持 IE,需 polyfill |
| 拦截能力 | 仅 get/set | 13 种拦截操作 |
五、进一步完善迷你系统
真实的 Vue 响应式系统还需要处理:
- 依赖清理:每次 effect 重新执行前清空旧依赖,避免无效触发。
- 嵌套 effect:用栈管理
activeEffect。 - 调度器:将更新推入微任务队列,避免同步多次触发。
- ref 与 computed:基于 effect 的派生值。
- 只读与浅响应:
readonly、shallowReactive等变体。
以依赖清理为例:
function effect(fn) {
const wrapped = () => {
cleanup(wrapped)
window.__activeEffect = wrapped
fn()
window.__activeEffect = null
}
wrapped.deps = []
wrapped()
return wrapped
}
function cleanup(effectFn) {
effectFn.deps.forEach(dep => {
const i = dep.indexOf(effectFn)
if (i > -1) dep.splice(i, 1)
})
effectFn.deps.length = 0
}
六、总结
Object.defineProperty 是 Vue 2 时代的选择,它在兼容性上有优势,但存在无法监听新增/删除属性、数组变异、初始化性能差等硬伤。Proxy 作为 ES6 的新特性,提供了更强大、更优雅的拦截能力,让 Vue 3 的响应式系统得以大幅简化并提升性能。
理解这两种方案的差异,不仅能帮助你更好地使用 Vue,也能在设计自己的状态管理库时做出更明智的技术选型。建议读者亲手实现一遍上述代码,感受从“数据劫持”到“依赖收集”再到“派发更新”的完整链路。
未经允许不得转载:任鹏个人博客 » 从零实现一个迷你 Vue 响应式系统:Object.defineProperty 与 Proxy 对比

