前言

本质上,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 = "上海市"     // 触发 set 

1.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    // 触发 deleteProperty

2.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?

  1. Reflect.set返回值(boolean),可以判断操作是否成功

  2. Reflect 的方法与 Proxy 捕获器一一对应,代码更一致

  3. 配合 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 按需拦截

更多推荐