概要
响应系统式vue的重要组成部分,我们都知道vue3中采用了proxy实现响应式数据的,那是怎么实现的呢?我们往下看
响应式数据与副作用函数
大家肯定会问:什么是副作用函数呢?如以下代码:
function effect(){
document.body.innerText = 'hello vue3'
}
当effect函数执行时,他会设置body的文本内容,但除了effect函数之外的任何函数都可能会读取或设置body的文本内容。也就是说effect函数的执行会直接或者间接影响到其他函数的执行,这时我们就会说effect函数产生了副作用。
理解了副作用函数,我们再说说什么是响应式数据,看一下代码
const obj = {text:'hello word'}
function effect(){
document.body.innerText = obj.text
}
obj.text = 'hello vue3'
看以上代码,effect中设置body的文本内容是使用了obj中text的值,当我们去改变text值的时候,我们会希望副作用effect函数要重新执行,但是很明显,以上代码无法做到这点。
响应式数据的基本实现
我们要怎么让obj变成响应式数据呢?想到这里,大家都会说用proxy把!没错就是用proxy,那具体要怎么做呢?
我们知道了改变obj的text值的时候,需要重新执行effect函数。我们可以观察到 effect函数中 有读取obj.text,这就会出发proxy的get操作,当哦我们修改obj.text值时,就会触发set操作。当然,我们可能不仅仅只有一个effect这个副作用函数,可能会有多个,这个时候我们会有什么办法呢?对的,我们可以把出发get操作的所有副作用函数存起来,在触发set操作时,拿出来全部执行一遍,那我们就会想到一下的写法:
const bucket = new Set()
const data = {text:'hello word'}
const obj = new Proxy(data,{
get(target,key){
bucket.add(effect)
return target[key]
},
set(target,key,newVal){
target[key] = newVal
bucket.forEach(fn=>fn())
return true
}
})
这里为什么需要用Set呢?就是为了使用Set的特性去重,因为我们每次执行effect的时候 都会执行一次get方法,如果我们使用正常Array来存储的话,那是不是会执行两次effect副作用函数了呢?
这样 我们在执行一次以下代码,看下是不是我们想要的效果
function effect (){
document.body.innerText = obj.text
}
effect()
setTimeout(()=>{
obj.text = 'hello vue3'
},1000)
我们会发现 当改变obj.text时,又执行了一次effect副作用函数,把body的文本内容更改了
设计一个完善的响应式系统
通过上面,我们以及实现了一个简单的响应式了,我们会发现直接通过名字effect来获取副作用函数,这种编码的方式不够灵活。副作用函数的名字我们应该是要可以随便取的,
所以我们要对以上的代码进行一个改造,我们可以把effect函数赋值给一个变量,这样我们就不会限制副作用函数的命名了。如以下代码:
//这里我们声明一个全局变量来存储被注册的副作用函数
let activeEffect
function effect(fn){
activeEffect = fn
fn()
}
//effect函数的使用方法就会变成传入一个函数为参数
effect(()=>{
document.body.innerText = obj.text
})
//我们在注册proxy代理的时候 就应该把effect变成activeEffect
const bucket = new Set()
const data = {text:'hello word'}
const obj = new Proxy(data,{
get(target,key){
if(activeEffect){ //这里需要判断一下是否存在副作用函数,存在就加进去,不存在就不需要
bucket.add(activeEffect)
}
return target[key]
},
set(target,key,newVal){
target[key] = newVal
bucket.forEach(fn=>fn())
return true
}
})
你觉得这个就是vue3响应式的原理的吗?不远远没有这么简单,我们可以随便测试一下,如果我们添加一个obj里面不存在的属性,也会执行这个副作用函数,因为我们的副作用函数没有和具体的key做绑定,只要有执行set方法,就会执行副作用函数,即使没有读取改变的属性。我们想要的对应关系应该如下:
target->key->effectFn
我们只有改变对应的key值才会执行对应key的副作用函数,那我们就对这个bucket
存储副作用函数的地方进行改造,如一下代码:
const bucket = new WeakMap()
const data = {text:'hello word'}
const Obj = new Proxy(data,{
get(target,key){
if(!activeEffect){
return target[key]
}
const desMap = bucket.get(target)
if(!desMap){
bucket.set(target,desMap=new Map())
}
const deps = desMap.get(key)
if(!deps){
desMap.set(key,deps = new Set())
}
deps.add(activeEffect)
return target[key]
},
set(target,key,newVal){
target[key] = newVal
const depsMap = bucket.get(target)
if(!depsMap ) return
const effect = depsMap.get(key)
effect && effect.forEach(fn=>fn())
return true
}
})
这里可能就有同学要问了,为什么这里要使用weakMap呢?因为:weakMap是弱引用,当它的key值不在被调用的的时候,会被垃圾回收机制回收掉,举一个很简单的例子
const weakmap = new WeakMap()
(function (){
const foo ={foo:1}
weakmap.set(foo,1)
})()
当我们执行完这个代码后,foo会立刻被垃圾回收机制回收掉,因为weakmap是一个弱引用,不会影响垃圾回收机制回收垃圾,当foo从内存中移除,我们便无法weakmap的key值,也就无法通过weakmap获取对象foo。
我们再把上面的代码稍微优化一下,做下封装处理,能够更好的复用:
const Obj = new Proxy(data,{
get(target,key){
track(target,key)
return target[key]
},
set(target,key,newVal){
target[key] = newVal
trigger(target,key)
}
})
function track(target,key) {
if(!activeEffect) return
const desMap = bucket.get(target)
if(!desMap){
bucket.set(target,desMap=new Map())
}
const deps = desMap.get(key)
if(!deps){
desMap.set(key,deps = new Set())
}
deps.add(activeEffect)
}
function trigger(target,key){
const depsMap = bucket.get(target)
if(!depsMap ) return
const effects = depsMap.get(key)
effects && effects.forEach(fn=>fn())
}
小结
以上就是基础的依赖收集,当然还是会有问题的,下一篇文章见,有错误的地方,希望给位同学可以指出,相互探讨!