使用过vue3的同学都知道被reactive和ref包裹的数据,会变成响应式数据。通过推导式的方式从头实现一个响应式原理。
响应式基础
Proxy
如果我们希望监听一个对象的相关操作,那么我们可以先创建一个代理对象(Proxy对象); 之后对该对象的所有操作,都通过代理对象来完成,代理对象可以监听我们想要对原对象进行哪些操作;
vue3之所以不再沿用vue2的方法主要是由于使用
object.defineProperty
存在一些缺陷,比如它不能监听对象的新增属性和删除属性,同时也无法监听到通过索引改变数组元素的操作。
Reflect
如果我们有Object可以做这些操作,那么为什么还需要有Reflect这样的新增对象呢?
- 所以在ES6中新增了
Reflect
,让我们这些操作都集中到了Reflect
对象上; Proxy
配合Reflect
使用,使用Reflect
更有语义- 另外在使用
Proxy
时,可以做到不操作原对象;
Reflect常用方法
◼ Reflect.get(target, propertyKey)
- 获取对象身上某个属性的值,类似于
target[name]
。
◼ Reflect.set(target, propertyKey, value)
- 将值分配给属性的函数。返回一个Boolean,如果更新成功,则返回true。
◼ Reflect.deleteProperty(target, propertyKey)
- 作为函数的delete操作符,相当于执行
delete target[name]
。
const info = {
name: '刘德华',
age: 18
}
const infoProxy = new Proxy(info, {
get(target, key) {
console.log(`读取了${key}`);
return Reflect.get(target, key)
},
set(target, key, value) {
console.log(`将${key}的值修改为${value}`);
Reflect.set(target, key, value)
},
deleteProperty(target, key) {
console.log(`删除了${key}属性`)
Reflect.deleteProperty(target, key)
}
})
infoProxy.name // 读取了name
infoProxy.age = 99 // 将age的值修改为99
delete infoProxy.age // 删除了age属性
响应式原理
响应式effect函数的实现
响应式就是当响应式数据发生变化时,对应的依赖也跟着执行。
let info = {
name: "peppa",
age: 4,
}
function effectFn1() {
console.log(info.name)
console.log(info.age)
}
function effectFn2() {
console.log(info.age * 2)
}
info.age = 10
effectFn1() // peppa // 10
effectFn2() // 20
可以看到我们每次需要对多个依赖进行调用,我们可以封装一个effect函数,对依赖进行保存,之后只需要遍历effectFns
挨个调用就行。
let info = {
name: "peppa",
age: 4,
}
const effectFns = []
function effect(fn) {
effectFns.push(fn)
}
effect(function effectFn1() {
console.log(info.name)
console.log(info.age)
})
effect(function effectFn2() {
console.log(info.age * 2)
})
info.age = 10
effectFns.forEach((fn) => fn()) // peppa // 10 // 20
当然我们实现的effect还存在很多问题,在后续我们会不断进行优化
目前我们收集的依赖是放到一个数组中来保存的,但是这里会存在数据管理的问题:
- 我们在实际开发中需要监听很多对象的响应式;
- 这些对象需要监听的不只是一个属性,它们很多属性的变化,都会有对应的响应式函数;
- 我们不可能在全局维护一大堆的数组来保存这些响应函数;
响应式依赖的收集
所以我们需要设计一个Depend
类,用于管理某个对象中某个属性的响应式数据
class Depend {
constructor() {
this.effectFns = []
}
add(fn) {
this.effectFns.push(fn) // 保存依赖
}
notify() {
this.effectFns.forEach((fn) => fn()) // 遍历依赖执行
}
}
重构effect函数
之前提到,我们为了能够对每个依赖进行正确的管理,不能将所有的依赖都保存在一个数组中,所以我们对之前的effect函数进行重构,以及对Depend
这个类进行一定的修改。
class Depend {
constructor() {
this.effectFns = []
}
add() {
if (activeFn) {
this.effectFns.push(activeFn) // 3.将全局的fn保存到dep的effectFns中
}
}
notify() {
this.effectFns.forEach((fn) => fn())
}
}
let activeFn = null
function effect(fn) {
activeFn = fn // 1.将fn保存在全局,用于在add中保存fn
activeFn() // 2.此处调用activeFn的目的是为了触发监听的get方法,用于收集依赖(后续实现)
activeFn = null // 4.保存完fn后置为null
}
监听对象的变化
在vue2中监听对象采用的方式是Object.defineProperty
的方式,在vue3在选择了new Proxy
的方式实现,在上面也提到了new Proxy
相较于Object.defineProperty
有哪些优势。我们可以封装一个reactive,之后将传入的对象能够转化为被监听的代理对象。
function reactive(target) {
const proxy = new Proxy(target, {
get(target, key) {
... // 收集依赖(后续实现)
return Reflect.get(target, key)
},
set(target, key, value) {
... // 触发依赖(后续实现)
Reflect.set(target, key, value)
},
})
return proxy
}
对象的依赖管理
对象的依赖管理在响应式原理中,算是一个难点,这个难点攻克了其他都不是问题了。
◼ 我们目前是创建了一个Depend对象,用来管理对于info变化需要监听的响应函数:
- 但是实际开发中我们会有不同的对象,另外会有不同的属性需要管理;
- 我们如何可以使用一种数据结构来管理不同对象的不同依赖关系呢?
要让每一个响应式对象中的key都映射一个dep实例。这样当key发生变化时,就可以及时的通知与该key所有的依赖。所以我们通过一种特殊的数据结构来进行保存,
- 全局有一个的
targetMap
,用于存放target
与depsMap
的映射 - 在
depsMap
这个map
中,用于存放key
与dep
的映射 - 数据结构如下图所示
// 使用weakMap的好处是弱引用,如果将target置为null是可以销毁的
const targetMap = new WeakMap()
// 封装函数:根据target和key获取对应depend实例
function getDep(target, key) {
// 根据对象target,知道对应的map对象
let depsMap = targetMap.get(target)
if (!depsMap) {
targetMap.set(target, (depsMap = new Map()))
}
// 根据key,找到对应的depend对象
let dep = depsMap.get(key)
if (!dep) {
depsMap.set(key, (dep = new Depend()))
}
return dep
}
收集与触发依赖
这一块就比较简单了,将对依赖的收集与触发执行时机添加到get和set中即可
function reactive(target) {
const proxy = new Proxy(target, {
get(target, key) {
const dep = getDep(target, key)
dep.add() // 收集依赖
return Reflect.get(target, key)
},
set(target, key, value) {
const dep = getDep(target, key)
Reflect.set(target, key, value)
dep.notify() // 触发依赖
},
})
return proxy
}
完整代码
class Depend {
constructor() {
this.effectFns = []
}
add() {
if (activeFn) {
this.effectFns.push(activeFn) // 4.将全局的依赖存到effectFns中
}
}
notify() {
this.effectFns.forEach((fn) => fn()) // 6.对所有依赖遍历挨个执行
}
}
const targetMap = new WeakMap()
function getDep(target, key) {
let depsMap = targetMap.get(target)
if (!depsMap) {
targetMap.set(target, (depsMap = new Map()))
}
let dep = depsMap.get(key)
if (!dep) {
depsMap.set(key, (dep = new Depend()))
}
return dep
}
let activeFn = null
function effect(fn) {
activeFn = fn // 1. 将依赖设置到全局
activeFn() // 2.执行fn(),用于触发get方法
activeFn = null
}
function reactive(target) {
const proxy = new Proxy(target, {
get(target, key) {
const dep = getDep(target, key) // 获取dep实例
dep.add(activeFn) // 3.对依赖进行收集
return Reflect.get(target, key)
},
set(target, key, value) {
const dep = getDep(target, key) // 获取dep实例
Reflect.set(target, key, value) // 5.触发依赖
dep.notify()
},
})
return proxy
}
// ====================测试代码========================
let info = reactive({
name: "peppa",
age: 4,
})
effect(function effectFn1() {
console.log(info.name)
console.log(info.age)
})
effect(function effectFn2() {
console.log(info.age * 2)
})
effect(function effectFn3() {
console.log(info.name + "是ping pig")
})
info.age = 10
输出结果
总结
- vue中所有的依赖会被effect函数包裹,会将该依赖函数保存到全局的变量中,并且调用依赖函数,其目的是为了被属性的get劫持。
- 在get中通过一种特殊的数据结构,获取到该属性唯一的dep实例,将之前全局的依赖函数保存到dep实例中。
- 之后对响应式对象发生更新时,也就是会被set所劫持,在set中拿到dep实例,并且对实例挨个调用。