鉴于之前看的手写Vue3源码约等于没看懂,写的博客也是一坨💩,我再来写一次加深理解...
什么是响应式系统?
比如,下面这段代码:
JavaScript
let a = 10
let b = 20
let c = a * b
console.log(c)
b = 30
console.log(c)
我们的意图是为了让b
发生变动时,控制台输出也发生改变,但是很显然上述代码做不到。我们想要实现的效果就是响应式系统意图解决的痛点。在数据发生变化时,所有使用了该数据的位置都会发生更新(也就是更新副作用),这一过程是自动的,也就避免了大量的冗余代码。
变量名解释
此处先介绍一下后面会用到的变量名和它们的简单释义,方便理解后续代码。
effect
:关于要某个数据的副作用函数,通过再次调用来实现数据更新track
:用于收集这些副作用的函数trigger
:用于触发这些副作用的函数dep
:也就是依赖,每个变量对应一个dep
,用于存储多个副作用函数(Set
类型,可以去重)depsMap
:用于存储dep,键值对形式,变量对应各自的dep(Map
类型)targetMap
:用于存储depsMap
,每个响应式对象对应一个depsMap
(WeakMap
类型,键为对象)reactive
:Vue
中的响应式类型,用于包裹对象类型数据为响应式数据activeEffect
:用于存储当前正要执行的effect
副作用函数ref
:Vue
中的响应式类型,用于包裹基本类型数据为响应式数据
逐步构建
JavaScript
const depsMap = new Map()
function track(key) {
let dep = depsMap.get(key)
if (!dep) {
depsMap.set(key, (dep = new Set()))
}
dep.add(effect)
}
function trigger(key) {
let dep = depsMap.get(key)
if (dep) {
dep.forEach(effect => { effect() })
}
}
上方的代码中实现了简易的track
和trigger
函数,可以窥见响应式的雏形。
首先定义了一个depsMap
,Map
类型,用于存储<key
-dep
>
后续定义了track
函数,实现了根据key
(属性)来从depsMap
中查找依赖,若取到值,则说明先前已经对该属性进行了追踪,后续只需要继续向该属性对应的dep
中添加effect
就可以;否则,创建新的dep
来存储副作用函数,并将其与对应的key
一同存入depsMap
。
之后又定义了trigger
函数,实现了根据key
在depsMap
中查找对应的依赖,如果找到则说明先前已经对该属性进行依赖收集,接下来遍历并执行dep
中存储的副作用函数就可以实现数据更新。
那么问题来了,目前实现的只是对于一个对象里的多个属性能实现响应式,如果有很多个对象怎么办?不能每个对象都写一遍吧😦???
没错,当然不行,这样的轮子显然不够好用,那么我们需要另一个能存储对象和对应的depsMap
的,类似<object
-depsMap
>形式的变量。一般来讲,会很自然地想到使用Map
但是实际上这样行不通,因为Map
的键只能使用字符串或者Symbol
类型,此时WeakMap
是更好的选择。
因为它的键只能使用对象类型!!这你受的了吗,简直和我们的需求天生一对
那么很自然的就催生了targetMap
用于存储<object
-depsMap
>,实现了对于不同对象收集它们各自属性的依赖(准确的讲,键应该是reactive
类型的对象)。
有了targetMap
,我们可以对代码做如下改进:
JavaScript
const targerMap = new WeakMap()
function track(target,key) {
let depsMap = targetMap.get(target)
if (!depsMap) {
depsMap = targetMap.set(target,(depsMap = new Map()))
}
let dep = depsMap.get(key)
if (!dep) {
depsMap.set(key, (dep = new Set()))
}
dep.add(effect)
}
function trigger(target,key) {
const depsMap = targetMap.get(target)
if (!depsMap) return
let dep = depsMap.get(key)
if (dep) {
dep.forEach(effect => { effect() })
}
}
也就是说,我们的逻辑是先根据target
确定响应式对象,然后在该对象中寻找key
属性,之后进行依赖收集或者更新依赖。
可是新的问题又出现了,总不能™一直手动调用track
和trigger
吧?
也就是说我们的目标是在effect
函数中获取(get
)或者修改(set
)响应式数据时,需要自动调用track
或者trigger
来实现依赖收集和依赖更新,那么就需要拦截GET
和SET
方法,来判断究竟调用track
还是trigger
。
在Vue2
中,官方使用的是Object.defineProperty()
来实现对于对象的代理,从而实现拦截的效果。
而在Vue3
中,官方使用的是ES6
的新特性,Proxy & Reflect
,它们相较于前者更加简洁高效。
(Vue3
不支持IE
,别踏🐎的用IE
了,给👴爬)
现在我们先来了解一下Proxy
和Reflect
:
Proxy
对象代理 ,简单来讲它就是隔在真实对象和用户之间的一个管家 。用户的代码对于对象的操作无非是获取和更改 两种,它们现在都要由这个管家 经手,而管家 可以在经手的过程中搞点事情,这就给了我们实现自动执行track
和trigger
的机会。
它的大致语法如下:
JavaScript
const obj = { a:10,b:20 }
let ProxyObj = new Proxy(obj,{
get(target,key,receiver?) {...}
set(target,key,value,receiver?) {...}
})
Proxy.a === obj.a
Proxy.b = 30
obj.b = 30 // 操作的结果等价
target
、key
、value
都好理解,但这个receiver
需要解释一下,它是一个可选参数,通常就是Proxy
对象本身,用于确保this
指向正确,在get
方法中保证上下文一致性。这里说实话不太好理解,举个例子:
JavaScript
const obj = {
a:10,
b:2,
alias:function() {
return this.a
}
}
let ProxyObj = new Proxy(obj,{
get(target,key,receiver?) {...}
set(target,key,value,receiver?) {...}
})
ProxyObj.alias()
这种情况就需要receiver
来保证this
指向正确
Reflect
专门用来配合Proxy
,方法与 Proxy
对象的方法一一对应,只要是 Proxy
对象的拦截方法,就能在 Reflect
对象上找到对应的方法。这使得 Proxy
对象可以方便地调用对应的 Reflect
方法,完成默认行为,作为修改行为的基础。
大致语法如下:
JavaScript
Reflect.set(target, Key, value[, receiver])
// receiver是可选参数
了解了它们的语法就可以开始搞事情了🤤
JavaScript
let product = {
price: 5,
quantity: 2
};
let proxiedProduct = new Proxy(product, {
get(target, key, receiver) {
console.log('Get was called with key ='+ key);
return Reflect.get(target, key, receiver);
},
set(target, key, value, receiver) {
console.log('Set was called with key ='+ key +'and value ='+ value);
return Reflect.set(target, key, value, receiver);
}
});
proxiedProduct.quantity = 4;
console.log(proxiedProduct.quantity);
好了,现在我们用代理实现了对于获取和修改操作的拦截,之后就是思考一下把track
和trigger
函数放在哪里的问题了
so,我们可以做如下改进:
JavaScript
let product = {
price: 5,
quantity: 2
};
let proxiedProduct = new Proxy(product, {
get(target, key, receiver) {
let result = Reflect.get(target, key, receiver);
track(target,key)
return result
},
set(target, key, value, receiver) {
let oldVal = target.key
if(oldVal != value) trigger(target,key)
return Reflect.set(target, key, value, receiver);
// 注意,Refelct.set()返回Boolean值
}
});
proxiedProduct.quantity = 4;
console.log(proxiedProduct.quantity);
现在已经实现了响应式,但是我们还是想要它更接近Vue3
的源码,所以我们稍稍封装一下:
JavaScript
function reactive(target) {
const handler = {
get(target, key, receiver) {
let result = Reflect.get(target, key, receiver);
track(target,key)
return result
},
set(target, key, value, receiver) {
let oldVal = target.key
if(oldVal != value) trigger(target,key)
return Reflect.set(target, key, value, receiver);
// 注意,Refelct.set()返回Boolean值
}
}
return new Proxy(target,handler)
}
const obj = { a:10,b:20 }
let product = reactive(obj)
欧吼,牛🍺,有模有样了,现在我们捋一捋到底是怎么个事儿:
-
获取
reactive
响应式对象的属性时,因为它是Proxy
代理过的对象,所以这时候拦截器会被触发,也就是get
函数会被调用JavaScriptget(target, key, receiver) { let result = Reflect.get(target, key, receiver); track(target,key) return result }
进入
get
函数以后,会首先取到该属性的值,然后调用track
方法,走我们之前讲过的那套流程,从targetMap
中寻找target
对应的depsMap
,没有则创建新的,有则在depsMap
中寻找key
对应的dep
,没有则创建,有则将副作用函数effect
放进dep
,实现依赖收集 -
修改
reactive
响应式对象的属性时,也会触发拦截器,此时调用set
函数JavaScriptset(target, key, value, receiver) { let oldVal = target.key if(oldVal != value) trigger(target,key) return Reflect.set(target, key, value, receiver); // 注意,Refelct.set()返回Boolean值 }
进入
set
函数后,会先取到原属性值oldVal
,之后比较新旧属性值,如果相同就不会更新,否则调用trigger
函数,从targetMap
中寻找target
对应的depsMap
,然后在depsMap
中寻找key
对应的dep
,并遍历执行所有的副作用函数,实现数据更新
不过现在仍然有一些问题,对于effect
,我们不想每次都手动调用一次,并且每次调用都会执行track
那么仿照Vue3
的做法,我们引入activeEffect
变量:
JavaScript
const targerMap = new WeakMap()
let activeEffect = null // 初始值置空
function effect(eff){
activeEffect = eff // 赋值为当前的副作用函数
activeEffect() // 执行当前的副作用函数
activeEffect = null // 还原为空
}
function track(target,key) {
if (activeEffect) { // 确保了只有在activeEffect非空时才执行下面的逻辑
let depsMap = targetMap.get(target)
if (!depsMap) {
depsMap = targetMap.set(target,(depsMap = new Map()))
}
let dep = depsMap.get(key)
if (!dep) {
depsMap.set(key, (dep = new Set()))
}
dep.add(effect)
}
}
function trigger(target,key) {
const depsMap = targetMap.get(target)
if (!depsMap) return
let dep = depsMap.get(key)
if (dep) {
dep.forEach(effect => { effect() })
}
}
现在实现了自动调用副作用函数,只需要把要执行的副作用函数作为回调函数传递给effect
函数就可以实现这一点,免去了手动调用,并且在收集依赖的环节中,避免了每次都执行track
中的逻辑,而是仅在有副作用函数时才执行,提升了效率
现在reactive搞的差不多了,我们来看看ref
刚刚写完reactive
,我们很顺理成章的就能搓出来一个ref
🤌
JavaScript
function ref(initialVal) {
return reactive({ value:initialVal })
}
竟然好像没什么毛病🤔
但是!!Vue
可不是这么写的(要是我能写出来我能在这码字??)
看下面⬇️
JavaScript
function ref(raw) {
const r = {
get value() {
track(r, 'value');
return raw;
},
set value(newVal) {
if(newVal != raw) { // 避免死循环
raw = newVal;
trigger(r, 'value');
}
}
};
return r;
}
当然了,这也是极简版本,只是大致展示了一下核心原理。对于获取和更新ref
来说,是比较直观的,但是如何在首次定义ref
类型变量时,如:
const myRef = ref(5)
让myRef等于5?这是一个稍显晦涩难懂的点,下面我们来解释一下
注意⚠️,此处使用了闭包的机制
raw
是 ref
函数的参数,属于 ref
函数作用域内的变量。r
对象中的 get value
和 set value
方法虽然定义在 ref
函数内部,但它们可以访问和操作 raw
变量。当 ref
函数执行完毕后,由于 r
对象中的这两个方法仍然引用着 raw
变量,所以 raw
变量不会被销毁,而是被保留在内存中。