前言
本质上,Vue3的响应式系统就是对JS对象特性的深度应用:
理解对象 → 理解属性描述符 → 理解如何拦截属性 ↓ Object.defineProperty(Vue2方案) ↓ Proxy + Reflect(Vue3方案) ↓ 依赖收集 + 自动更新 = 响应式系统
一、深入理解JS对象
1.1 属性描述符
var obj = { name: "why", age: 18 } // 数据属性描述符 Object.defineProperty(obj, "address", { value: "北京市", // 属性值 configurable: false, // 是否可删除/可重新配置 enumerable: true, // 是否可枚举(for...in 能否遍历到) writable: false // 是否可写入 }) delete obj.address // configurable:false,无法删除 obj.address = "上海市" // writable:false,无法修改关键:描述符定义的属性,
configurable/enumerable/writable默认全是false。字面量定义的属性默认全是true。
1.2 存取属性描述符——拦截属性的读写
这是连接"面向对象"和"响应式"的第一个桥梁:
var obj = { name: "why", _address: "北京市" } Object.defineProperty(obj, "address", { enumerable: true, configurable: true, get: function() { console.log("获取了一次address的值") return this._address }, set: function(value) { console.log("设置了address的值,新值:" + value) this._address = value } }) console.log(obj.address) // 触发 get obj.address = "上海市" // 触发 set1.3 限制对象
var obj = { name: 'why', age: 18 } // Level 1:禁止扩展(不能添加新属性) Object.preventExtensions(obj) // Level 2:密封对象(不能添加 + 不能删除) Object.seal(obj) // Level 3:冻结对象(不能添加 + 不能删除 + 不能修改) Object.freeze(obj)理解这些方法对后续理解 Vue 的readonly/shallowReadonly很有帮助。
二、监听对象变化
2.1 Vue2方案:Object.defineProperty
遍历所有 key,给每个属性加get/set:
const obj = { name: "why", age: 18 } Object.keys(obj).forEach(key => { let value = obj[key] Object.defineProperty(obj, key, { get: function() { console.log(`监听到obj对象的${key}属性被访问了`) return value }, set: function(newValue) { console.log(`监听到obj对象的${key}属性被设置值`) value = newValue } }) }) obj.name = "kobe" // 触发 set obj.age = 30 // 触发 set obj.height = 1.88 // 新增属性,无法监听Vue2方案的缺点:
无法监听新增属性(所以需要
Vue.set/this.$set)无法监听删除属性(所以需要
Vue.delete)必须遍历每个 key
2.2 Vue3方案:Proxy
Proxy可以代理整个对象,而不是逐个属性:
const obj = { name: "why", age: 18 } const objProxy = new Proxy(obj, { get: function(target, key) { console.log(`监听到对象的${key}属性被访问了`, target) return target[key] }, set: function(target, key, newValue) { console.log(`监听到对象的${key}属性被设置值`, target) target[key] = newValue } }) objProxy.name = "kobe" // 触发 set objProxy.height = 1.88 // 新增属性也能监听!Proxy 的捕获器
const objProxy = new Proxy(obj, { get: function(target, key) { // 拦截读取:obj.key console.log(`${key}属性被访问`) return target[key] }, set: function(target, key, newValue) { // 拦截设置:obj.key = val target[key] = newValue }, has: function(target, key) { // 拦截 in 操作符:key in obj console.log(`${key}的in操作被拦截`) return key in target }, deleteProperty: function(target, key) { // 拦截 delete 操作:delete obj.key console.log(`${key}的delete操作被拦截`) delete target[key] } }) "name" in objProxy // 触发 has delete objProxy.name // 触发 deleteProperty2.3 Reflect
Reflect提供了与Proxy捕获器一一对应的静态方法:
const objProxy = new Proxy(obj, { get: function(target, key, receiver) { console.log("get---------") return Reflect.get(target, key) // 替代 target[key] }, set: function(target, key, newValue, receiver) { console.log("set---------") const result = Reflect.set(target, key, newValue) // 有返回值,表示成功/失败 if (result) { // 设置成功 } else { // 设置失败 } } })为什么用 Reflect 而不是直接操作 target?
Reflect.set有返回值(boolean),可以判断操作是否成功Reflect的方法与Proxy捕获器一一对应,代码更一致配合
receiver参数可以正确处理 this 指向
三、手写响应式系统
有了Proxy+Reflect,我们就可以开始构建一个完整的响应式系统了。
3.1 响应式函数的手动触发
把需要重新执行的函数收集起来,数据变化时统一执行。
// 封装一个响应式函数 let reactiveFns = [] function watchFn(fn) { reactiveFns.push(fn) } const obj = { name: "why", age: 18 } watchFn(function() { console.log(obj.name) // 依赖了 obj.name }) watchFn(function() { console.log(obj.name, "demo function") }) // 数据变了,手动触发 obj.name = "kobe" reactiveFns.forEach(fn => fn()) // 手动通知问题很明显:每次都要手动调用reactiveFns.forEach
3.2 把函数收集封装成 Depend 类
class Depend { constructor() { this.reactiveFns = [] } addDepend(reactiveFn) { this.reactiveFns.push(reactiveFn) } notify() { this.reactiveFns.forEach(fn => fn()) } } const depend = new Depend() function watchFn(fn) { depend.addDepend(fn) }有了这个类,我们需要解决的问题变成:如何让 Proxy 在 set 的时候自动调用 depend.notify()?
3.3 Proxy 自动通知
在 Proxy 的set中调用depend.notify():
const objProxy = new Proxy(obj, { get: function(target, key, receiver) { return Reflect.get(target, key, receiver) }, set: function(target, key, newValue, receiver) { Reflect.set(target, key, newValue, receiver) depend.notify() // 自动通知 } }) objProxy.name = "kobe" // 自动触发所有依赖函数但这有一个问题:所有属性共用同一个depend对象。修改name会触发age的依赖函数也执行
3.4 每个属性独立的 Depend
核心数据结构:WeakMap → Map → Depend
// WeakMap<target, Map<key, Depend>> const targetMap = new WeakMap() function getDepend(target, key) { // 根据 target 获取 Map let map = targetMap.get(target) if (!map) { map = new Map() targetMap.set(target, map) } // 根据 key 获取 Depend let depend = map.get(key) if (!depend) { depend = new Depend() map.set(key, depend) } return depend }数据结构图:
targetMap (WeakMap) ├── obj → Map { "name" → Depend1, "age" → Depend2 } └── info → Map { "address" → Depend3 }为什么用 WeakMap?当目标对象被垃圾回收时,WeakMap 中对应的 key 也会被自动回收,不会造成内存泄漏。
改造 Proxy:
const objProxy = new Proxy(obj, { get: function(target, key, receiver) { return Reflect.get(target, key, receiver) }, set: function(target, key, newValue, receiver) { Reflect.set(target, key, newValue, receiver) const depend = getDepend(target, key) // 精确获取该 key 的 Depend depend.notify() // 只通知关心这个 key 的函数 } }) objProxy.name = "kobe" // 只触发 name 的依赖函数 objProxy.age = 100 // 只触发 age 的依赖函数3.5 自动收集依赖
目前还有一个问题:依赖(哪些函数依赖了哪些属性)还是我们手动watchFn添加的。
真正需要的是:当函数执行时,如果它访问了objProxy.name,就自动把这个函数收集为name的依赖。
核心思路:利用 Proxy 的get捕获器来自动收集
let activeReactiveFn = null function watchFn(fn) { activeReactiveFn = fn // 记录当前正在执行的响应式函数 fn() // 执行函数 → 访问属性 → 触发 get → 收集依赖 activeReactiveFn = null } // Proxy get 中自动收集 const objProxy = new Proxy(obj, { get: function(target, key, receiver) { const depend = getDepend(target, key) depend.addDepend(activeReactiveFn) // 自动收集 return Reflect.get(target, key, receiver) }, set: function(target, key, newValue, receiver) { Reflect.set(target, key, newValue, receiver) const depend = getDepend(target, key) depend.notify() } })流程:
watchFn(fn) → activeReactiveFn = fn → fn() 执行 → 访问 objProxy.name → 触发 get 捕获器 → depend.addDepend(fn) ← 自动收集 → activeReactiveFn = null
3.6 优化 Depend 类
使用Set代替数组(去重)、封装depend()方法:
let activeReactiveFn = null class Depend { constructor() { this.reactiveFns = new Set() // Set 自动去重 } depend() { if (activeReactiveFn) { this.reactiveFns.add(activeReactiveFn) // 自动收集 } } notify() { this.reactiveFns.forEach(fn => fn()) } }3.7 封装成reactive()函数
将所有逻辑封装,最终得到完整响应式实现:
// ============ Vue3 响应式核心 ============ let activeReactiveFn = null class Depend { constructor() { this.reactiveFns = new Set() } depend() { if (activeReactiveFn) { this.reactiveFns.add(activeReactiveFn) } } notify() { this.reactiveFns.forEach(fn => fn()) } } // 响应式函数 function watchFn(fn) { activeReactiveFn = fn fn() activeReactiveFn = null } // 依赖管理:WeakMap<target, Map<key, Depend>> const targetMap = new WeakMap() function getDepend(target, key) { let map = targetMap.get(target) if (!map) { map = new Map() targetMap.set(target, map) } let depend = map.get(key) if (!depend) { depend = new Depend() map.set(key, depend) } return depend } // 核心:将普通对象变成响应式对象 function reactive(obj) { return new Proxy(obj, { get: function(target, key, receiver) { const depend = getDepend(target, key) depend.depend() // 自动收集依赖 return Reflect.get(target, key, receiver) }, set: function(target, key, newValue, receiver) { Reflect.set(target, key, newValue, receiver) const depend = getDepend(target, key) depend.notify() // 自动通知更新 } }) } // ============ 使用示例 ============ const objProxy = reactive({ name: 'why', age: 18 }) const infoProxy = reactive({ address: '广州市', height: 1.88 }) watchFn(() => { console.log(infoProxy.address) }) infoProxy.address = '北京市' // 自动触发上面的回调! const foo = reactive({ name: 'foo' }) watchFn(() => { console.log(foo.name) }) foo.name = 'bar' // 自动触发回调 foo.name = 'hhh' // 自动触发回调四、Vue2 的写法对比
同样的依赖收集逻辑,Vue2 用Object.defineProperty实现:
function reactive(obj) { Object.keys(obj).forEach(key => { let value = obj[key] Object.defineProperty(obj, key, { get: function() { const depend = getDepend(obj, key) depend.depend() return value }, set: function(newValue) { value = newValue const depend = getDepend(obj, key) depend.notify() } }) }) return obj }两者的核心区别:
| Vue2 (defineProperty) | Vue3 (Proxy) | |
|---|---|---|
| 拦截对象 | 逐个属性 | 整个对象 |
| 新增属性 | 无法监听 | 可以监听 |
| 删除属性 | 无法监听 | 可以监听 |
| 数组索引/长度 | 有问题 | 支持 |
| 性能 | 初始化时遍历所有 key | 按需拦截 |