本文已参与「新人创作礼」活动,一起开启掘金创作之路。
vue3响应式原理——分支切换
上次简单的实现了vue3响应式原理,track收集依赖,trigger触发依赖可戳该链接查看,这次我们来聊一聊分支切换。
不知道有没有小白和我一样懵逼,啥,分支切换?这是什么鬼,我只知道git的分支切换,莫慌,🤔 我们先看一个🌰
例子
如果我们要收集的依赖函数长这样
let obj={
flag:true,
count:1,
name:"zhangsan"
}
let newObj=new Proxy(obj,{
get(){
...
},
set(){
...
}
})
//...
//主要看这里⬇️
effect(()=>{
console.log("执行了effect函数")
newObj.flag? document.getElementById("app").textContent=newObj.name : "字符串11"
})
effect()
console.log(bucket)
effect函数执行时,会读取flag 和name的值,此时会对我们的flag和name进行依赖收集,我们在这里可以打印一下,最终bucket的值是什么
没问题,的确对flage和name的值进行依赖的收集了。 接下来,我们试着去修改flag的值
newObj.flag=false
此时flag为false,
1.document.getElementById("app") 的值会是“字符串11”,
2.同时会打印两次 “执行了effect函数”,一次是收集依赖的时候,一次是触发依赖的时候
如果我们此时再修改
newObj.name="lisi"
我们会发现又打印了一次 “执行了effect函数”
这是怎么回事?🤔️🤔️
没错,就是我们收集依赖的时候,收集到了name属性的依赖函数,所以在修改name属性值的时候,才会出发effect函数。
问题
可是,这样真的合理吗?
1.effect函数中,如果flag为false,我们只会走false的分支,我们不care newOBj.name的值是多少,我在修改name值的时候,又会触发effect函数,这样的更新是必要的吗?
往更大点说,如果我们在执行effect函数时,收集了n多个属性的依赖,每修改一个属性就去触发effect函数,😱 难以想象
简单来讲,这种收集依赖的方式会导致不必要的更新
思考
如何解决呢? 首先,什么是合理的? 比如我第一次flag为true时,会收集flag和name的依赖
当我们修改flag的值时,去触发依赖,此时,我们的effect函数应该处于一个空的状态,等待下次用户再访问属性时,做新的一次依赖收集
解决方式
即:每次执行effect函数时,我们可以先把他从从所有与之关联的依赖集合中删除
1.首先,effect函数修改如下
let effect=(fn)=>{
const effectFn=()=>{
activeEffect=effectFn
fn()
}
effectFn.deps=[] //用来存储所有与该副作用函数相依赖的集合
effectFn()
}
2.我们在收集依赖时,将所有
function track(target,key){
let depMaps=bucket.get(target)
if(!depMaps){
bucket.set(target,(depMaps=new Map()))
}
let deps=depMaps.get(key)
if(!deps){
depMaps.set(key,(deps=new Set()))
}
deps.add(activeEffect)
//将其添加进去 [deps,deps,deps] ,这里的deps就是{flag: deps } =>flag的依赖函数集
//这里的deps就是{name: deps } =>name的依赖函数集
activeEffect.deps.push(deps)
}
3.再在effect中添加代码
let effect=(fn)=>{
const effectFn=()=>{
//清空掉effect函数关联的依赖集合
for(let i=0;i<effectFn.deps.length;i++){
//比如:i=0 这里的deps 就是flag收集的依赖集合
let deps=effectFn.deps[i]
//我们删除掉deps里的依赖的集合,这样再去setter()修改的时候,
//发现deps的长度使0,就不会触发依赖了
deps.delete(effectFn)
}
effectFn.deps.length=0
activeEffect=effectFn
fn()
}
effectFn.deps=[] //用来存储所有与该副作用函数相依赖的集合
effectFn()
}
4.当我们这时候,去查看效果的时候,发现!!
what!这是什么情况,为啥栈溢出了?
我们思考这样一个场景
const set=new Set([1]);
set.forEach(function(){
set.delete(1)
set.add(1)
console.log(999)
})
forEach在遍历set时,如果一个值已经被访问过,但该值被删除并重新添加到集合,如果此时forEach还没有结束,那么该值会被重新访问 解决这个问题很简单 我们可以在触发依赖的时候,构造一个set集合,并遍历
//触发依赖
function trigger(target,key){
let depMaps=bucket.get(target)
let effects=depMaps.get(key)
const effectToRun=new set(effects)
effectToRun.forEach(effectFn()=>{
effectFn()
})
//deps && deps.forEach(effect => {
// effect()
//});
}
完整代码如下:
let obj={
flag:true,
count:1,
name:"zhangsan"
}
let activeEffect=null
let effect=(fn)=>{
//清空effectFn 的deps
const effectFn=()=>{
cleanUp(effectFn)
activeEffect=effectFn
fn()
}
effectFn.deps=[]
effectFn()
}
let bucket=new WeakMap()
let newObj=new Proxy(obj,{
get(target,key){
track(target,key)
return Reflect.get(target,key)
},
set(target,key,value){
Reflect.set(target,key,value)
trigger(target,key)
return true
},
})
//分支切换
function cleanUp(effectFn){
for (let i = 0; i < effectFn.deps.length; i++) {
const deps=effectFn.deps[i]
deps.delete(effectFn)
}
effectFn.deps.length=0
}
function track(target,key){
let depMaps=bucket.get(target)
if(!depMaps){
bucket.set(target,(depMaps=new Map()))
}
let deps=depMaps.get(key)
if(!deps){
depMaps.set(key,(deps=new Set()))
}
deps.add(activeEffect)
// console.log(key);
activeEffect.deps.push(deps)
}
function trigger(target,key){
let depMaps=bucket.get(target)
let effects=depMaps.get(key)
let effectTorun=new Set(effects) //set保证具有唯一性
effectTorun.forEach(effectFn => {
effectFn()
});
}
effect(()=>{
console.log("执行了effect函数")
newObj.flag? document.getElementById("app").textContent=newObj.name : "123"
})
console.log(bucket)
newObj.flag=false
总结:
分支切换,本质上是在收集依赖时,把和该副作用函数有联系的属性所收集的effects,收集起来,再在执行副作用函数时清空掉,当副作用函数执行完毕后,会重新建立联系