切换分支和cleanup
如果在副作用函数中传入的fn是一个三元函数,比如document.body.innertext = obj.ok ? obj.text : 'not'。在这种情况下,obj.ok是true还是false就决定了分支的走向。
如果obj.ok是true,那么在代理的数据中就会涉及到obj.ok 和 obj.text两个属性的读取操作,就会有两个deps收集到fn这个副作用函数。如果是false,由于'not'是固定的值,不会影响其他变量,所以应该是在proxy代理中只有obj.ok这个属性涉及到读取操作,就只有一个deps收集到fn这个副作用函数。
但是在vue响应系统的作用和实现二的代理中,即使是obj.ok的值为false,修改obj.text还是会执行副作用函数fn。其他代码参考vue响应系统的作用和实现二最后的代码。
//调用effect
effect(
()=>{
//会执行三次,第一次是effect中的fn(),第二次obj.ok的set操作,第三次是obj.text的set操作
console.log('effect run')
document.body.innerText = obj.ok ? obj.text : 'not'
}
)
setTimeout(()=>{
obj.ok = false
obj.text = 'hello,vue!'
},1000)
解决这个问题的思路是每次副作用函数执行时,我们先把它从所有与之关联的依赖集合中删除,在进行依赖添加。
let activeEffect = null
function effect(fn){
const effectFn = () =>{ //新增
//调用cleanup函数完成清除工作
cleanup(effectFn) //新增
//当effectFn执行时,将其设置为当前激活的副作用函数
activeEffect = effectFn //修改
fn()
}
//effectFn.deps用来存储所有与该副作用函数相关联的依赖集合
effectFn.deps = [] //新增
effectFn() //新增
}
const data = { ok:true, text:'hello,world' }
const bucket = new WeakMap()
const obj = new Proxy(data,{
get(target,key){
track(target,key)
return Reflect.get(target, key)
},
set(target,key,newVal){
target[key] = newVal
trigger(target,key)
}
})
function track(target,key){
if(!activeEffect) 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)
//deps就是一个与该副作用函数相关联的依赖集合
//相当于哪个deps添加了activeEffect副作用函数,就把他收集起来,到时候清除的时候统一循环清除
activeEffect.deps.push(deps) //新增
}
function trigger(target,key){
const depsMap = bucket.get(target)
if(!depsMap) return
const effects = depsMap.get(key)
//由于set.prototype.forEach的规范问题,为了避免无限执行
const effectsToRun = new Set(effects) //新增
effectsToRun && effectsToRun.forEach(fn=>fn()) //新增
}
//清除集合依赖函数
function cleanup(effectFn) {
for(let i= 0; i<effectFn.deps.length; i++){
//deps就是track函数中那个depsMap.get(key),依赖集合
const deps = effectFn.deps[i]
//将effectFn从依赖集合中移除,实际上就是deps.add(activeEffect)的反义
deps.delete(effectFn)
}
//重置数组长度
effectFn.deps.length = 0
}
effect(
()=>{
//会执行两次
console.log('effect run')
document.body.innerText = obj.ok ? obj.text : 'not'
}
)
setTimeout(()=>{
obj.ok = false
obj.text = 'hello,vue!'
},1000)
其中,语言规范问题,Set.prototype.forEach会无限执行,所以添加新的Set来进行循环。
感兴趣的小伙伴可以查看一下ECMA2020中的695-696页。
以上参考霍春阳老师的《Vue.js设计与实现》,有兴趣的朋友可以看一下原书,更加详细·-·