Stories

Detail Return Return

《vue.js設計與實現》——嵌套的 effect 與 effect 棧 - Stories Detail

effect 是可以發生嵌套的
vue.js的下渲染函數其實就是在一個effect中執行的
當組件發生嵌套時,例如Foo組件渲染了Bar組件

// Bar組件
const Bar = {
    render() { }
}
// Foo組件嵌套渲染了Bar組件
const Foo = {
    render() {
        return <Bar />
    }
}

此時就發生了effect嵌套

effect(() => {
    Foo.render()
    // 嵌套
    effect(() => {
        Bar.render()
    })
})

我們要搞清楚effect為什麼要嵌套,不嵌套會發生什麼

// 原始數據
const data = { foo: true, bar: true }
// 全局變量
let temp1, temp2;
// effectFn1嵌套effectFn2
effect(function effectFn1() {
    console.log('effectFn1執行了');
    effect(function effectFn2() {
        console.log('effectFn2執行了');
        // 在effectFn2中讀取了obj.foo
        temp2 = obj.bar

    })
    // 在effectFn1中讀取了obj.foo
    temp1 = obj.foo
})

上面的代碼中,effectFn1執行後會導致effectFn2執行
effectFn2執行後會讀取obj.bar的值,effectFn1中也會讀取obj.foo的值
所以他們的結構應該是:

data -> foo -> effectFn1
     -> bar -> effectFn2

我們修改foo的時候,會執行effectFn1effectFn2,而修改bar只會執行effectFn2
但結果卻是修改foo只執行了effectFn2

出現這個問題是因為我們拿activeEffect保存副作用函數,並且它只能保存一個,當effectFn2執行的時候會直接覆蓋effectFn1
解決方法:創建一個副作用函數棧effectStack,副作用函數執行時,將當前副作用函數壓入棧中,在其執行完畢後將它從棧中彈出,並始終讓activeEffect指向棧頂的副作用函數,這樣就可以避免嵌套的副作用函數互相覆蓋的問題

// 定義一個全局變量,用來存儲當前激活的副作用函數
let activeEffect;
// 棧
const effectStack = []
function effect(fn) {
    const effectFn = () => {
        // 利用 cleanup 函數清除 effectFn 中存儲的副作用函數
        cleanup(effectFn);
        // 當effectFn執行時,activeEffect保存當前effectFn副作用函數
        activeEffect = effectFn
        // 在調用副作用函數之前將當前副作用函數壓入棧中
        effectStack.push(effectFn);
        fn(); // 真正執行的副作用函數
        // 當前副作用函數執行完畢後,將當前副作用函數彈出棧,並把activeEffect還原為之前的值
        effectStack.pop();
        // activeEffect還原為之前的值
        activeEffect = effectStack[effectStack.length - 1]
    }
    // 定義一個deps數組,用來收集與該副作用函數相關的依賴集合
    effectFn.deps = []
    effectFn()
}

上面的代碼圖解起來大概就是這樣:


activeEffect始終等於棧頂的副作用函數,每次執行完後棧頂的副作用函數就被彈出,這樣就可以避免嵌套的副作用函數互相覆蓋的問題
驗證 :

// 驗證
// 全局變量
let temp1, temp2;
// effectFn1嵌套effectFn2
effect(function effectFn1() {
    console.log('effectFn1執行了');
    effect(function effectFn2() {
        console.log('effectFn2執行了');
        // 在effectFn2中讀取了obj.foo
        temp2 = obj.bar
    })
    // 在effectFn1中讀取了obj.foo
    temp1 = obj.foo
})

setTimeout(() => {
    console.log('修改==========');
    obj.foo = false
}, 1000);

完整代碼:

// 定義一個全局變量,用來存儲當前激活的副作用函數
let activeEffect;
// 棧
const effectStack = []
function effect(fn) {
    const effectFn = () => {
        // 利用 cleanup 函數清除 effectFn 中存儲的副作用函數
        cleanup(effectFn);
        // 當effectFn執行時,activeEffect保存當前effectFn副作用函數
        activeEffect = effectFn
        // 在調用副作用函數之前將當前副作用函數壓入棧中
        effectStack.push(effectFn);
        fn(); // 真正執行的副作用函數
        // 當前副作用函數執行完畢後,將當前副作用函數彈出棧,並把activeEffect還原為之前的值
        effectStack.pop();
        // activeEffect還原為之前的值
        activeEffect = effectStack[effectStack.length - 1]
    }
    // 定義一個deps數組,用來收集與該副作用函數相關的依賴集合
    effectFn.deps = []
    effectFn()
}

// 重置依賴關係
function cleanup(effectFn) {
    // 遍歷 effectFn.deps 數組
    for (let i = 0; i < effectFn.deps.length; i++) {
        // deps 是依賴集合
        const deps = effectFn.deps[i]
        // 將 effectFn 從依賴集合中移除
        // 也就是在依賴收集的時候的deps,這個deps其實就是key對應的Set集合
        // deps.delete(effectFn)也就是刪除key對應的Set集合中的activeEffect
        deps.delete(effectFn)
    }
    // 最後重置 effectFn.deps 數組
    effectFn.deps.length = 0
}

// 存儲副作用函數的桶
const bucket = new WeakMap()
function track(target, key) {
    // 沒有activeEffect,直接return
    if (!activeEffect) return target[key]
    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 -> Set構造函數
    // 把activeEffect當作依賴收集給deps
    deps.add(activeEffect)
    // deps就是與當前副作用函數存在聯繫的依賴集合
    // 把依賴集合添加到activeEffect.deps數組中 -> effectFn.deps
    activeEffect.deps.push(deps)
}

function trigger(target, key) {
    const depsMap = bucket.get(target)
    if (!depsMap) return
    // 獲取依賴集合effects[] -> activeEffect.deps[] -> effectFn.deps
    const effects = depsMap.get(key)
    // 用一個新Set保存依賴集合,直接使用effects會出現無限循環
    // 因為effectFn函數內部調用了cleanup函數在執行刪除操作,而track函數內部的deps.add(activeEffect)又在新增,導致無限循環
    const effectsToRun = new Set(effects)
    // 調用effectFn函數,並且effectFn函數內部會調用cleanup重置依賴關係
    effectsToRun.forEach(effectFn => effectFn())
}

const data = { foo: true, bar: true }

const obj = new Proxy(data, {
    get(target, key) {
        track(target, key);
        return target[key]
    },
    set(target, key, newValue) {
        target[key] = newValue
        trigger(target, key);
    }
})

// 驗證
// 全局變量
let temp1, temp2;
// effectFn1嵌套effectFn2
effect(function effectFn1() {
    console.log('effectFn1執行了');
    effect(function effectFn2() {
        console.log('effectFn2執行了');
        // 在effectFn2中讀取了obj.foo
        temp2 = obj.bar
    })
    // 在effectFn1中讀取了obj.foo
    temp1 = obj.foo
})

setTimeout(() => {
    console.log('修改==========');
    obj.foo = false
}, 1000);


參考資料:vue.js設計與實現
本篇文章為霍春陽老師的《vue.js設計與實現》的核心知識點總結,感興趣的朋友可以支持原版書籍。

user avatar bukenengdeshi Avatar tongbo Avatar haiyong Avatar
Favorites 3 users favorite the story!
Favorites

Add a new Comments

Some HTML is okay.