问题描述:在上一篇中,通过weakmap,map,Set数据结构,建立了代理对象中 target ,key, 副作用函数之前的对应关系,使得我们可以处理不存在的属性,避免不必要的程序执行,但上一篇的完善结构中还有一个问题出现,测试如下程序:
const data = {ok:true,text:'hello world'}
let bucket = new WeakMap()
let activeEffect;
function effect(fn){
activeEffect = fn;
fn()
}
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)
}
function trigger(target,key){
let depsMap = bucket.get(target)
if(!depsMap) return
const effects = depsMap.get(key)
effects && effects.forEach(fn=>fn())
}
const obj = new Proxy(data,{
get(target,key){
track(target,key)
return target[key]
},
set(target,key,newVal){
target[key] = newVal;
trigger(target,key)
}
})
effect(
function effectFn(){
console.log('effect')
document.body.innerText = obj.ok ? obj.text :'not'
}
)
setTimeout(()=>{
obj.ok = false
obj.text = 'hello vue3'
})
完善结构后我们可以看到打印如下:
理论上在副作用函数中的判断后,我无论再去怎么改变obj.text的值,副作用函数都不应该再去执行一次,所以本次代码就是要解决如上问题;
解决思路很简单,在副作用函数执行前,删除所有与复函数关联的依赖属性即可
function effect(fn){
const effectFn = ()=>{
cleanup(effectFn)
activeEffect = effectFn;
fn()
}
// activeEffect.deps用来存储所有与该副作用函数相关联的依赖集合
effectFn.deps=[]
// 执行副作用函数
effectFn()
}
function cleanup(effectFn){
for(let i=0;i<effectFn.deps.length;i++){
effectFn.deps[i].delete(effectFn)
}
// 重置数组
effectFn.deps.length = 0
}
track函数添加deps函数集合
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数组中
activeEffect.deps.push(deps)
}
写到这里可以避免复函数的遗留了,但我们运行程序后会发现无线循环
问题出现在这里
01 function trigger(target, key) {
02 const depsMap = bucket.get(target)
03 if (!depsMap) return
04 const effects = depsMap.get(key)
05 effects && effects.forEach(fn => fn()) // 问题出在这句代码
06 }
解释:
在trigger函数中,effects存储在Set数据结构中,在遍历执行复函数时,cleanup会先删除当前复函数,然后在注册复函数的时候又把当前复函数添加到Set集合中,而此时Set集合还在遍历,就会导致无线循环,举例解释如下代码:
01 const set = new Set([1])
02
03 set.forEach(item => {
04 set.delete(1)
05 set.add(1)
06 console.log('遍历中')
07 })
语言规范中对此有明确的说明:在调用 forEach 遍历 Set 集合时,如果一个值已经被访问过了,但该值被删除并重新添加到集合,如果此时 forEach 遍历没有结束,那么该值会重新被访问。因此,上面的代码会无限执行。解决办法很简单,我们可以构造另外一个 Set 集合并遍历它:
01 const set = new Set([1])
02
03 const newSet = new Set(set)
04 newSet.forEach(item => {
05 set.delete(1)
06 set.add(1)
07 console.log('遍历中')
08 })
回到 trigger 函数,我们需要同样的手段来避免无限执行:
01 function trigger(target, key) {
02 const depsMap = bucket.get(target)
03 if (!depsMap) return
04 const effects = depsMap.get(key)
05
06 const effectsToRun = new Set(effects) // 新增
07 effectsToRun.forEach(effectFn => effectFn()) // 新增
08 // effects && effects.forEach(effectFn => effectFn()) // 删除
09 }
整体代码构建如下:
const data = {ok:true,text:'hello world'}
let bucket = new WeakMap()
let activeEffect;
function effect(fn){
const effectFn = ()=>{
cleanup(effectFn)
activeEffect = effectFn;
fn()
}
// activeEffect.deps用来存储所有与该副作用函数相关联的依赖集合
effectFn.deps=[]
// 执行副作用函数
effectFn()
}
function cleanup(effectFn){
for(let i=0;i<effectFn.deps.length;i++){
effectFn.deps[i].delete(effectFn)
}
// 重置数组
effectFn.deps.length = 0
}
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)
activeEffect.deps.push(deps)
}
function trigger(target,key){
let depsMap = bucket.get(target)
if(!depsMap) return
const effects = depsMap.get(key)
const effectsToRun = new Set(effects)
// effects && effects.forEach(fn=>fn())
effectsToRun.forEach(effectFn=>effectFn())
}
const obj = new Proxy(data,{
get(target,key){
track(target,key)
return target[key]
},
set(target,key,newVal){
target[key] = newVal;
trigger(target,key)
}
})
effect(
function effectFn(){
console.log('effect')
document.body.innerText = obj.ok ? obj.text :'not'
}
)
setTimeout(()=>{
obj.ok = false
obj.text = 'hello vue3'
})
结果查看: