这篇单纯为本人学习《vue设计与实现》做的笔记,写得并不好,建议直接阅读这本书
1.响应式数据与副作用函数
要想理解vue中的响应式系统原理,就必须知道什么是副作用函数和响应式数据,如下所示
const obj = {text:'hello world'}
function effect(){
document.body.innerText = obj.text
}
函数effect执行会设置元素文本,读取文本内容,其他函数也会读取和设置文本内容,effect会直接或间接影响到其他函数执行,这样的函数就称为副作用函数. 那什么是响应式数据呢,如上,我们希望我们修改obj中的值,副作用函数自动重新执行,那么这个对象就是响应式数据
2.响应式数据基本实现
实现响应式数据我们要做的就是 1.当副作用函数执行时,触发obj.text的读取操作,并将该副作用函数收集起来 2.当修改obj.text时,触发obj.text的设置操作,将副作用函数拿出来执行 这里定义一个叫bucket(桶)的数据集合
vue3是用Proxy来代理对象实现响应式数据的,这里也用Proxy
const bucket = new Set()//存放副作用函数
//数据
const obj = {text:'hello world'}
//对数据进行代理
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
}
)
function effect(){ //副作用函数
document.body.innerText = obj.text
}
effect()//执行副作用函数,读取obj.text,将副作用函数收集
setTimeout(()=>{ //2s后修改obj.text,触发set操作,bucket中副作用函数依次执行
obj.text = "hello vue3"
},2000)
3.相对完善的响应式系统
按照上面编码,只要函数名不叫effect我们这个响应式系统就失效了,还是有很多缺点,很不完善,我们要让即使副作用函数为匿名函数,也可以触发,也可以被收集,所以我们需要提供一个注册副作用函数的机制
//全局变量存储被注册的副作用函数
let activeEffect
//effect函数用于注册副作用函数
function effect(fn){
//当effect注册副作用函数时,将副作用函数fn赋值给activeEffect
activeEffect = fn
//执行一下副作用函数
fn()
}
//这样,即使副作用函数为匿名函数,我们也可以收集起来
effect(()=>{
document.body.innerText = obj.text
})
const obj = new Proxy(data,{
get(target,key){
if(activeEffect){
bucket.add(activeEffect) //将activeEffect收集到桶中
}
return target[key]
},
set(target,key,newVal){
target[key] = newVal
bucket.forEach(fn=>fn())//将副作用函数依次执行
return true
}
)
这样我们注册副作用函数的机制就创建成功了,但是发生了以下操作呢
effect(()=>{
console.log("触发啦")
document.body,innerText = obj.text
})
setTimeout(()=>{
obj.notExist = "hello vue3"
},2000)
请问,会打印几次“触发啦”,答案是两次,这是因为我们在读取时把副作用函数加入到了bucket里面,此时设置obj.notExist会触发obj的set操作,会把bucket中的副作用函数拿出来执行,所以要解决这个问题,就必须重新设置bucket的结构,让obj中每个属性和对应的副作用函数一一对应 那么,bucket的结构该怎么设计呢 观察一下下面的代码,这段代码有三个角色 1.被读取代理对象obj 2.被读取字段名text 3.被注册副作用函数effectFn
effect(function effectFn (){
document.body,innerText = obj.text
})
那么,就可以为这3个角色创建一种树型结构
如果当两个副作用函数访问同一个对象的属性值
effect(function effectFn1(){
obj.text
})
effect(function effectFn2(){
obj.text
})
那么,他们的关系对应如下
如果,一个副作用欢呼声读取同一个代理对象不同的属性
effect(function effectFn(){
obj.text
obj.foo
})
那么,他们的关系应该为
如果,不同的副作用函数访问代理对象不同的属性
effect(function effectFn1(){
obj.text
})
effect(function effectFn2(){
obj.foo
})
他们关系为
接下来,就是按照这数据结构实现这个bucket
我们的bucket是这样的结构
const bucket = new WeakMap() //这里为什么用WeakMap,因为这里我们用对象作为key,WeakMap只能用引用数据类型做键名,WeakMap对键名中的引用数据类型是弱引用,键所指的对象可以被垃圾回收
const obj = new Proxy(data,{
get(target,key){
track(target,key)
return target[key]
},
set(target,key,newVal){
target[key] = newVal
trigger(target,key)//将副作用函数从bucket中取出来执行
return true
}
)
//这里封装两个函数,分别为读取操作时将副作用函数添加到bucket中,另一个为将副作用函数取出执行的函数
//在get函数中调用track函数追踪
function track(target,key){
//没有activeEffect直接返回
if(!activeEffect) return
let depsMap = bucket.get(target)
if(!depsMap){ //在bucket中代理对象的map集合,没有就新建与代理对象关联
bucket.set(target, (depsMap = new Map()))
}
//根据key从map集合中找到对应的set集合
let deps = depsMap.get(key)
if(!deps){ //没有则新建与key对应的set集合添加到depsMap中
depsMap.set(key,(deps = new Set()))
}
deps.add(activeEffect)//将副作用函数添加至该对应mset集合中
retuen target[key]
}
//设置操作中的函数
function trigger(target,key){
const depsMap = bucket(target)//取出target的map集合
if(!depsMap) return //没有直接返回
const effects = depsMap.get(key)
effects && effects.forEach(fn=>fn()) //将对应的所有副作用函数全部执行
}
到这里,代理数据和他的属性与副作用函数就一一对应,bucket是每一个代理对象(target)作为键的WeakMap类型, bucket中的每一个Map类型的对象是由target的key作为键的value为存储着副作用函数的set类型的值.