問題描述:在上一篇中,通過weakmap,map,Set數(shù)據(jù)結構,建立了代理對象中 target ,key, 副作用函數(shù)之前的對應關系,使得我們可以處理不存在的屬性,避免不必要的程序執(zhí)行,但上一篇的完善結構中還有一個問題出現(xiàn),測試如下程序:
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'
})
完善結構后我們可以看到打印如下:

理論上在副作用函數(shù)中的判斷后,我無論再去怎么改變obj.text的值,副作用函數(shù)都不應該再去執(zhí)行一次,所以本次代碼就是要解決如上問題;
解決思路很簡單,在副作用函數(shù)執(zhí)行前,刪除所有與復函數(shù)關聯(lián)的依賴屬性即可
function effect(fn){
const effectFn = ()=>{
cleanup(effectFn)
activeEffect = effectFn;
fn()
}
// activeEffect.deps用來存儲所有與該副作用函數(shù)相關聯(lián)的依賴集合
effectFn.deps=[]
// 執(zhí)行副作用函數(shù)
effectFn()
}
function cleanup(effectFn){
for(let i=0;i<effectFn.deps.length;i++){
effectFn.deps[i].delete(effectFn)
}
// 重置數(shù)組
effectFn.deps.length = 0
}
track函數(shù)添加deps函數(shù)集合
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數(shù)組中
activeEffect.deps.push(deps)
}
寫到這里可以避免復函數(shù)的遺留了,但我們運行程序后會發(fā)現(xiàn)無線循環(huán)

問題出現(xiàn)在這里
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函數(shù)中,effects存儲在Set數(shù)據(jù)結構中,在遍歷執(zhí)行復函數(shù)時,cleanup會先刪除當前復函數(shù),然后在注冊復函數(shù)的時候又把當前復函數(shù)添加到Set集合中,而此時Set集合還在遍歷,就會導致無線循環(huán),舉例解釋如下代碼:
01 const set = new Set([1])
02
03 set.forEach(item => {
04 set.delete(1)
05 set.add(1)
06 console.log('遍歷中')
07 })
語言規(guī)范中對此有明確的說明:在調(diào)用 forEach 遍歷 Set 集合時,如果一個值已經(jīng)被訪問過了,但該值被刪除并重新添加到集合,如果此時 forEach 遍歷沒有結束,那么該值會重新被訪問。因此,上面的代碼會無限執(zhí)行。解決辦法很簡單,我們可以構造另外一個 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 函數(shù),我們需要同樣的手段來避免無限執(zhí)行:
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用來存儲所有與該副作用函數(shù)相關聯(lián)的依賴集合
effectFn.deps=[]
// 執(zhí)行副作用函數(shù)
effectFn()
}
function cleanup(effectFn){
for(let i=0;i<effectFn.deps.length;i++){
effectFn.deps[i].delete(effectFn)
}
// 重置數(shù)組
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'
})
結果查看:
