这篇单纯为本人学习《vue设计与实现》做的笔记,写得并不好,建议直接阅读这本书
4.分支切换与cleanup
首先我们要知道什么是分支切换,如代码所示
const data = {
ok:true,
text:"hello word"
}
const obj = new Proxy(data,{...})
effect(function effectFn(){
document.body.innerText = obj.ok?obj.text:"not"
})
这个副作用函数里是一个三元表达式,当obj.ok发生不同变化时,会执行不同的代码,这就是分支切换 拿上面的函数来说,当obj.ok为true时,会读取obj.text字段,副作用函数会被当做依赖函数所收集,这确实是我们想要的结果。但是,当我们修改obj.ok为false时,obj.text并不会被读取,但是之前的副作用函数已经被作为依赖函数收集到bucket中,我们修改obj.text值时,副作用函数依然会执行,这会造成不必要的更新,代码如下所示
const data = {
ok:true,
text:"hello word"
}
const obj = new Proxy(data,{...})
effect(function effectFn(){
document.body.innerText = obj.ok?obj.text:"not"
})
obj.ok = false
//这会触发更新,但是此时obj.text为false,所以不在读取obj.text,我们再怎么修改obj.text 应该是不能再触发更新,但是结果并不是这样
obj.text = "hello vue3" //这时会触发更新
原因正如上面所说,当obj.ok为true时,已经读取了obj.text,副作用函数已经存进obj.text的依赖集合中,即使后面不读取,修改的话,依然可以从 依赖集合中拿到副作用函数来执行
解决方法就是在每次执行副作用函数之前,将副作用函数从相关依赖集合中移除,问题就迎刃而解 要想把一个副作用函数从所有与之相关联的依赖集合中移除,就需要知道哪些依赖集合含有它
let activeEffect
function effet(fn){
const effectFn = ()=>{
activeEffect = effectFn
fn()
}
effectFn.deps=[] // activeEffect.deps用来存储与该副作用函数相关联的依赖集合
effectFn() //执行副作用函数
}
function track(target,key){
if(!active) return
let depsMap = bucket.get(target)
if(!depsMap) bucket.set(target,(depsMap = new Map()))
let deps = depsMap.get(key)
if(!deps) depsMap.set(key,(deps = new Set()))
deps.add(activeEffect)
//将这个依赖集合添加到activeEffect.deps中
activeEffect.push(deps)
}
有了这联系,根据effectFN.deps回去所有与之相关联的依赖集合,进而将副作用函数从中移除
let activeEffect
function effect(fn){
const effectFn = ()=>{
//调用cleanup函数完成去除工作
cleanup(effectFn)
activeEffect = effectFn
fn()
}
effectFn.deps = []
effectFn()
}
cleanup函数实现
function cleanup(effectFn){
for(let i=0;i<effectFn.deps.length;i++){
deps是当前副作用函数的依赖集合
const deps = effectFn.deps[i]
//将effectFn从依赖集合中去除
deps.delete(effectFn)
}
}
但是,运行这段代码时候,会出现死循环
function trigger(target,key){
const depsMap = bucket.get(target)
if(!depsMap) return
const effects = depsMap.get(key)
effects && effects.forEach(fn=>fn()) //问题就在这句代码中
}
上面的代码执行,会调用副作用函数,副作用函数中会调用cleanup函数将依赖清除,但是副作用函数执行会读取到代理对象的属性,这会导致将副作用函数重新收集,对于effects来说,这个循环还在执行,因为这个依赖集合是Set类型,Set类型在遍历时,一个值被访问过,但该值被删除并重新添加到集合中去,如果此时遍历没有结束,那么值会被重新访问,一直循环下去,解决方法就是用另一个set集合去遍历
function trigger(target,key){
const depsMap = bucket.get(target)
if(!depsMap) return
const effects = depsMap.get(key)
const effectsToRun = new Set(effects)
effectsToRun.forEaach(fn=>fn())
}
但是当我们的代码是这样
effect(()=>{
obj.foo = obj.foo+1
})
在这个代码中,既会读取obj.foo的值,也会设置obj.foo的值,读取时触发tragger操作,将函数收集,更改obj.foo的值又会触发track操作,将副作用函数执行,问题是副作用函数正在执行,里面又有读取和设置的操作,就要开始下一次副作用函数执行,这就会导致无限递归调用自己 怎么解决?当副作用trigger触发的副作用函数和正在执行的副作用函数相同则不触发
function trigger(target,key){
const depsMap = bucket.get(target)
if(!depsMap) return
const effects = depsMap.get(key)
const effectsToRun = new Set()
effects && effects.forEach(effectFn=>{
//如果trigger触发执行的副作用函数与当前执行的副作用函数相同,则不触发执行
if(effectFn !== activeEffect){
effectsToRun.add(effectFn)
}
})
effectsToRun.forEaach(fn=>fn())
}
5.嵌套的effect与effect栈
副作用函数会发生嵌套,
effect(function effectFn1(){
effect(function effectFn2(){...})
})
什么时候会发生副作用函数的嵌套,在vue中渲染函数render是放在一个effect中执行的,当组件发生嵌套,副作用函数就会发生嵌套
cosnt Foo={
render(){
return /../
}
}
effect(()=>{
Foo.render()
})
//当组件嵌套
const Bar={
render(){...}
}
const Foo={
render(){
return <Bar/>
}
}
//此时的副作用函数
effect(()=>{
Foo.render()
effect(()=>{
Bar.render()
})
})
vue为什么要设计得可嵌套呢
effect(function effectFn1(){
console.log(1)
effect(function effectFn2(){
console.log(2)
obj.bar
})
obj.fo
})
在这段函数中我们修改obj.fo会发现输出为1,2,2,问题就在第3次输出,我们修改了obj.fo的值,应该打印的是1,但是打却是2,所以,这不符合预期 这个问题就出在effect 和activeEffect上
let activeEffect
function effect(fn){
const effectFn = ()=>{
//调用cleanup函数完成去除工作
cleanup(effectFn)
activeEffect = effectFn
fn()
}
effectFn.deps = []
effectFn()
}
由上面的代码可知,我们是把副作用函数直接赋值给activeEffect,当函数发生嵌套,里层的函数会覆盖掉外层的函数,activeEffect收集的是最里面的副作用函数。 为了解决这个问题,vue设计了应该effectStack的函数栈,将当前的函数压入栈中,当副作用函数执行完再弹出,让activeEffect始终取到函数栈中最顶部的函数
let activeEffect
const effectStack=[]
function effect(fn){
const effectFn = ()=>{
//调用cleanup函数完成去除工作
cleanup(effectFn)
activeEffect = effectFn
effectStack.push(effectFn)//将副作用函数压入栈中
fn()
//副作用函数执行完,从栈中弹出
effectStack.pop()
activeEffect = effectStack[effectStack.length-1]
}
effectFn.deps = []
effectFn()
}