在搞懂Vue2 - Vue3 响应式原理前,我们首先要认识JavaScript
中的Object.defineProperty
、Proxy
、map
、weakMap
、Set
、Reflect
这几个知识点。
如果已经明白这些知识点,我们可以直接导航跳到"响应式原理"。
我们希望监听对象中的属性被设置或获取的过程时可以使用它们。
vue2中的响应式原理利用了Object.defineProperty
访问器属性,它设计的初衷,其实不是为了去监听截止一个对象中所有的属性的。
const obj = {
name: "蜘蛛侠",
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 = "死侍"
obj.age = 30
console.log(obj.name)
console.log(obj.age)
obj.height = 1.88
如果我们想监听更加丰富的操作,比如新增属性、删除属性,那么Object.defineProperty
是无能为力的
由于Object.defineProperty
的缺陷,vue3使用了在ES6中,新增的一个Proxy
类,这个类从名字就可以看出来,是用于帮助我们创建一个代理的。
也就是说,如果我们希望监听一个对象的相关操作,那么我们可以先创建一个代理对象(Proxy对象
);
之后对该对象的所有操作,都通过代理对象来完成,代理对象可以监听我们想要对原对象进行哪些操作;
const obj = {
name: "蜘蛛侠",
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
}
})
console.log(objProxy.name)
console.log(objProxy.age)
objProxy.name = "死侍"
objProxy.age = 30
console.log(obj.name)
console.log(obj.age)
响应式的原理主要分4步,封装响应式函数,收集依赖,对象的依赖管理,监听对象的变化。
凡是传入到watchFn
的函数,就是需要响应式的
首先通过activeReactiveFn变量
暂存并且收集到当前需要收集的响应式函数,它们最后会推入到Depend类
类中的reactiveFns
数组中(第二步的内容)
然后我们会立刻执行下这个响应式函数
// 暂存并且收集到当前需要收集的响应式函数,最后会推入到depend类中reactiveFns
let activeReactiveFn = null
// 封装一个响应式的函数
function watchFn(fn) {
activeReactiveFn = fn
fn()
activeReactiveFn = null
}
我们要设计一个类,这个类用于管理某一个对象的某一个属性的所有响应式函数,这么说太复杂了我们实际的演练一遍。
只要构造了Depend类
,那么我们收集的activeReactiveFn
变量这时候就要派上用场了,我们通过判断,内存中activeReactiveFn
存在了,就在变量叫reactiveFns
的 Set
类型数组内推入这个响应式函数(set的不可重复特性)。
Depend类
通过调用depend
方法收集响应式函数并且推入到reactiveFns
数组中。
再通过调用notify
方法触发所有Depend类
中收集到的函数。
// 响应式依赖的收集
class Depend {
constructor() {
this.reactiveFns = new Set()
}
depend() {
if (activeReactiveFn) {
this.reactiveFns.add(activeReactiveFn)
}
}
notify() {
this.reactiveFns.forEach(fn => {
fn()
})
}
}
实际开发中我们会有不同的对象需要绑定响应式,另外这些对象也会有不同的属性需要管理
我们用WeakMap
对象和Map
对象管理响应式的数据依赖
我们会通过WeakMap
对象的特性弱引用提升性能,逐级的去获取到map
对象存储的depend
类
// 封装一个获取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
}
通过 Object.defineProperty
的方式(vue2采用的方式);
通过new Proxy
的方式(vue3采用的方式);
主要讲一讲Vue3:
封装完reactive函数后
我们用reactive
函数去绑定一个对象,当我们发生数据操作,比如get
获取属性proxy
代理会自动通过getDepend
函数会生成一个depend
类,然后调用depend.depend()
收集所有响应式函数,并且把结果反射出去
再比如set
修改属性时,代理会先反射修改的值,再自动通过getDepend
函数会生成一个depend
类,然后调用depend.notify()
把收集到的响应式函数触发一次,以此来达到一个完整的响应式流程。
// vue2监听对象的变化
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
}
//vue3
function reactive(obj) {
return new Proxy(obj, {
get: function(target, key, receiver) {
// 根据target.key获取对应的depend
const depend = getDepend(target, key)
// 给depend对象中添加响应函数
// depend.addDepend(activeReactiveFn)
depend.depend()
return Reflect.get(target, key, receiver)
},
set: function(target, key, newValue, receiver) {
Reflect.set(target, key, newValue, receiver)
// depend.notify()
const depend = getDepend(target, key)
depend.notify()
}
})
}
!!!重点
以Vue3举例,源码是这样的
// 保存当前需要收集的响应式函数
let activeReactiveFn = null
// 封装depend类收集依赖
class Depend {
constructor() {
this.reactivefns = new Set()
}
depend() {
if (activeReactiveFn) {
this.reactivefns.add(activeReactiveFn)
}
}
notify() {
this.reactivefns.forEach(fn => { fn() })
}
}
// 封装一个响应式的函数
const watchFn = (fn) => {
activeReactiveFn = fn
fn()
activeReactiveFn = null
}
// 封装一个获取depend函数
let targetMap = new WeakMap()
const 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
}
// 封装代理数据
const reactive = (obj) => {
return new Proxy(obj, {
get(target, key, receiver) {
const depend = getDepend(target, key)
depend.depend()
return Reflect.get(target, key, receiver)
},
set(target, key, value, receiver) {
Reflect.set(target, key, value, receiver)
const depend = getDepend(target, key)
depend.notify()
return true
}
})
}
//1、 监听对象的属性变量: Proxy(vue3)/Object.defineProperty(vue2)
const objProxy = reactive({
name: "死侍", // depend对象
age: 18 // depend对象
})
const infoProxy = reactive({
address: "广州市",
height: 1.88
})
//2、 绑定响应式函数
watchFn(() => {
console.log(infoProxy.address)
})
//3、 修改对象的内容
infoProxy.address = "北京市"
watchFn(() => {
console.log(objProxy .name)
})
objProxy.name = "蜘蛛侠"
我们一步步剖析原理,现在讲一下我们绑定响应式后的过程
1、我们给对象绑定上响应式,监听对象的属性变量
const objProxy = reactive({
name: "死侍", // depend对象
age: 18 // depend对象
})
const infoProxy = reactive({
address: "广州市",
height: 1.88
})
2、绑定响应式函数
watchFn(() => {
console.log(infoProxy.address)
})
我们在响应式函数内传入一个() => {console.log(infoProxy.address)}
箭头函数
这时候因为infoProxy
已经成功绑定了响应式
所以infoProxy.address
就会触发proxy
代理的get
方法
getDepend
方法会传入target
({address: "广州市",height: 1.88}
),key
(address
)两个参数
由于是第一次传入,它会通过map
和weakmap
重新生成一个depend
类并返回
并且这个Depend
类会自动收集() => {console.log(infoProxy.address)}
箭头函数到类自己的reactivefns
中,而后反射出来
Depend
类的depend
方法收集到了() => {console.log(infoProxy.address)}
箭头函数然后存入reactivefns
中
反射出代理的对象,我们的控制台就会打印“广州市”
3、 修改对象的内容
infoProxy.address = "北京市"
我们修改infoProxy.address
的内容时,会触发proxy
的set
方法
它返回一个depend
类,由于之前get方法触发时已经收集到了target
({address: "广州市",height: 1.88}
),key
(address
)
所以我们这次set
也会传入的target
(自身),key
(键)
这样就会无比准确的获取到get
收集到的那个depend
类
这个类的reactivefns
中保存着() => {console.log(infoProxy.address)}
箭头函数
我们set
方法中通过depend
类中的方法notify
再一次触发了这个() => {console.log(infoProxy.address)}
箭头函数
控制台就会打印出我们修改属性后的内容"北京市",完成一个响应式闭环~
后续的代码,是同理的就不多解释了
watchFn(() => {
console.log(objProxy .name)
})
objProxy.name = "蜘蛛侠"