动态

详情 返回 返回

《vue.js設計與實現》——調度執行 - 动态 详情

調度性的定義:當我們執行trigger動作觸發副作用函數重新執行時,有能力決定副作用函數的時機、次數以及方式

控制執行時機

先來看看如何決定副作用函數的執行方式

const data = { foo: 1 }
const obj = new Proxy(data, {}); // 為了演示,省略代理配置
effect(() => {
    console.log(obj.foo);
})
obj.foo++
console.log('結束了');

在上面的代碼中,會先調用副作用函數執行obj.foo,然後執行自增操作,在自增操作的過程中會觸發effect的再次調用,最後打印結束了

所以結果應該是 :1、2、結束了
但是我們希望的結果是:1、結束了、2
這個時候如何實現不調整代碼結構的情況下實現呢?
其實我們思考一下,在JS中,關於任務的執行無非就是兩個維度,同步任務、微任務;同步任務會有限執行,微任務會在同步任務執行完後執行。有了這個想法之後我們就可以着手實現了
此時我們需要對副作用函數設置一個選項參數options(這個參數其實就是一個對象,在裏面寫一些配置啥的來控制一些流程),比如允許用户指定調度器

effect(
    () => {
        console.log(obj.foo);
    },
    // 選項參數options
    {
        // 設置調度器
        scheduler(fn) {
            // 省略其它...
        }
    }
)

這樣,用户在調用副作用函數的時候可以傳入第二個參數,這個參數是一個對象,對象中可以設置scheduler屬性
當用户設置了scheduler屬性時,在effect函數內部我們就需要把options選項掛載到對應副作用函數上

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

有了調度函數,我們就可以在trigger函數執行副作用時調用用户傳入的調度器函數,控制權就在用户手上了

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 && effects.forEach(effectFn => {
        // 如果trigger觸發執行的副作用函數與當前正在執行的副作用函數不同則執行,否則不執行
        if (effectFn !== activeEffect) {
            // 收集依賴
            effectsToRun.add(effectFn)
        }
    })
    // 調用effectFn函數,並且effectFn函數內部會調用cleanup重置依賴關係
    effectsToRun.forEach(effectFn => {
        // 新增,若有調度函數則調用調度函數,傳入副作用函數
        if (effectFn.options.scheduler) {
            effectFn.options.scheduler(effectFn)
        } else {
            // 否則直接執行副作用函數
            effectFn()
        }
    })
}

在上面的代碼中,用户就可以手動控制副作用函數的執行了,當調度器存在時將當前副作用函數傳入調度器並執行,否則直接執行

const data = { foo: 1 }
const obj = new Proxy(data, {})
effect(
    () => {
        console.log(obj.foo);
    },
    // 選項參數options
    {
        // 設置調度器
        scheduler(fn) {
            // 將函數放入宏任務隊列執行
            setTimeout(fn)
        }
    }
)
obj.foo++
console.log('結束了');


最後執行的結果就是:1、結束了、2
這裏其實就是把修改obj.foo關聯trigger的副作用函數執行交給調度函數執行了,在調度器在中把副作用函數放在了異步任務隊列,所以同步會先執行:1、結束了,異步後執行:2

控制執行次數

除了控制它的執行順序外,我們還需要能夠控制他們的執行次數,這也是調度函數非常重要的一個功能

effect(
    () => {
        console.log(obj.foo);
    }
)
obj.foo++
obj.foo++


上面的代碼中,按照正常順序執行的結果應該是: 1、2、3
其實相同的操作,中間的2作為過渡可以省略掉,只需要輸出1、3即可,中間2的打印是多餘的
這個時候我們就需要使用一個變量來記錄副作用函數是否正在執行,如果正在執行則什麼都不做,隊列執行完後才把阻塞放開
我們要對調度器稍作修改

// 定義一個任務隊列
const jobQueue = new Set();
// 定義一個任務隊列執行函數,這是一個Promise的微任務,並且是resolve狀態,調用後可以直接.then
const p = Promise.resolve();
// 定義一個變量來標記是否正在刷新隊列
let isFlushing = false;
function flushJob() {
    // 如果正在刷新隊列,則什麼都不做
    if (isFlushing) return
    // 當任務隊列中有任務時,將isFlushing設置為true,表示正在刷新隊列
    isFlushing = true
    // 在微任務隊列中刷新jobQueue任務隊列
    p.then(() => {
        jobQueue.forEach(job => job())
    }).finally(() => {
        // 刷新完成後將isFlushing設置為false
        isFlushing = false
    })
}
// 修改副作用函數
effect(
    () => {
        console.log(obj.foo);
    },
    // 選項參數options
    {
        // 設置調度器
        // 在修改的時候執行trigger函數,在其內部調用調度器函數並傳入副作用函數
        scheduler(fn) {
            // 每次調度時,將副作用函數添加到任務隊列中
            jobQueue.add(fn)
            // 調用flushJob刷新隊列
            flushJob() // 修改操作的時候,啓動隊列任務刷新
        }
    }
)
obj.foo++
obj.foo++


流程解釋:
在上面的代碼中,我們會首先打印obj.foo:1,然後obj.foo++,此時觸發trigger函數,在trigger函數內部會判斷調度器是否存在,若調度器存在則將副作用函數傳入調度器中,調度器函數會將副作用函數收集到任務隊列,然後開始執行flushJob函數,啓動隊列任務刷新,flushJob函數內部會首先判斷isFlushing變量,初始的時候為false,所以剛進入時會執行一次,執行前把isFlushing設置為true,然後執行Promise的微任務,微任務執行完後會執行finally中的函數恢復isFlushingfalse

整段代碼的效果:
連續對obj.foo執行自增操作,會同步執行兩次scheduler調度函數,也就是説任務隊列jobQueue會被添加兩次,但是由於Set函數具有自動去重能力,所以objQueue中只會存在一個副作用函數,也就是當前執行的副作用函數,並且flushJob函數也會執行兩次,但是由於isFlushing變量的存在,所以只會執行一次微任務,只有隊列的任務執行完畢後,isFlushing變量才會被恢復為false,由於jobQueue隊列中只有一個副作用函數,所以只會執行一次,當它執行的時候obj.foo已經變為3了,所以最終的輸出就會變為:1、3

在這裏一定要注意幾點:
obj.foo++的自增操作是同步任務,所以會優先執行
jobQueue.forEach(job => job())這一行實際上是在Promise.resolve()這個微任務內執行的
所以,我們剛開始會輸出一次1,而後連續修改obj.foo++,修改關聯的副作用函數會被放入微任務隊列,當微任務隊列執行完畢時,同步任務的obj.foo++已經執行到3了,所以最後obj.foo的輸出就是3
其實在vue.js中連續多次修改同一個響應式數據只會觸發一次更新,跟這個思路是相同,只不過vue.js做了一個更完善的調度器

完整代碼

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

const jobQueue = new Set();
// 定義一個任務隊列執行函數,這是一個Promise的微任務,並且是resolve狀態,調用後可以直接.then
const p = Promise.resolve();
// 定義一個變量來標記是否正在刷新隊列
let isFlushing = false;
function flushJob() {
    // 如果正在刷新隊列,則什麼都不做
    if (isFlushing) return
    // 當任務隊列中有任務時,將isFlushing設置為true,表示正在刷新隊列
    isFlushing = true
    // 在微任務隊列中刷新jobQueue任務隊列
    p.then(() => {
        jobQueue.forEach(job => job())
    }).finally(() => {
        // 刷新完成後將isFlushing設置為false
        isFlushing = false
    })
}

// 重置依賴關係
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 && effects.forEach(effectFn => {
        // 如果trigger觸發執行的副作用函數與當前正在執行的副作用函數不同則執行,否則不執行
        if (effectFn !== activeEffect) {
            // 收集依賴
            effectsToRun.add(effectFn)
        }
    })
    // 調用effectFn函數,並且effectFn函數內部會調用cleanup重置依賴關係
    effectsToRun.forEach(effectFn => {
        // 新增,若有調度函數則調用調度函數,傳入副作用函數
        if (effectFn.options.scheduler) {
            effectFn.options.scheduler(effectFn)
        } else {
            // 否則直接執行副作用函數
            effectFn()
        }
    })
}

const data = { foo: 1 }

const obj = new Proxy(data, {
    get(target, key) {
        track(target, key);
        return target[key]
    },
    set(target, key, newValue) {
        target[key] = newValue
        trigger(target, key);
    }
})
// 驗證
effect(
    () => {
        console.log(obj.foo);
    },
    // 選項參數options
    {
        // 設置調度器
        // 在修改的時候執行trigger函數,在其內部調用調度器函數並傳入副作用函數
        scheduler(fn) {
            // 每次調度時,將副作用函數添加到任務隊列中
            jobQueue.add(fn)
            // 調用flushJob刷新隊列
            flushJob() // 修改操作的時候,啓動隊列任務刷新
        }
    }
)
// 控制執行時機的驗證
// obj.foo++
// console.log('結束了');

// 控制執行次數的驗證
obj.foo++
obj.foo++

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

Add a new 评论

Some HTML is okay.