JavaScript进阶:从面向对象到Vue3响应式原理
前言
本质上,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?
-
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 | 按需拦截 |
更多推荐
所有评论(0)