webpack中tapable原理詳解友酱,一起學(xué)習(xí)任務(wù)流程管理

學(xué)習(xí)webpack源碼時(shí)晴音,總是繞不開(kāi)tapable,越看越覺(jué)得它晦澀難懂缔杉,但只要理解了它的功能锤躁,學(xué)習(xí)就會(huì)容易很多。

簡(jiǎn)單來(lái)說(shuō)或详,有一系列的同步系羞、異步任務(wù),我希望它們可以以多種流程執(zhí)行霸琴,比如:

  • 一個(gè)執(zhí)行完再執(zhí)行下一個(gè)椒振,即串行執(zhí)行
  • 一塊執(zhí)行梧乘,即并行執(zhí)行澎迎;
  • 串行執(zhí)行過(guò)程中,可以中斷執(zhí)行选调,即有熔斷機(jī)制
  • 等等

tapable庫(kù)夹供,就幫我們實(shí)現(xiàn)了多種任務(wù)的執(zhí)行流程,它們可以根據(jù)以下特點(diǎn)分類(lèi):

  • 同步sync学歧、異步async**:task是否包含異步代碼
  • 串行series罩引、并發(fā)parallel**:前后task是否有執(zhí)行順序
  • 是否使用promise
  • 熔斷bail**:是否有熔斷機(jī)制
  • waterfall:前后task是否有數(shù)據(jù)依賴(lài)

舉個(gè)例子,如果我們想要多個(gè)同步的任務(wù) 串行執(zhí)行枝笨,只需要三個(gè)步驟:初始化hook、添加任務(wù)揭蜒、觸發(fā)任務(wù)執(zhí)行

// 引入 同步 的hook
const { SyncBailHook } =  require("tapable");
// 初始化
const tasks = new SyncBailHook(['tasks'])
// 綁定一個(gè)任務(wù)
tasks.tap('task1', () => {
    console.log('task1', name);
})
// 再綁定一個(gè)任務(wù)
tasks.tap('task2', () => {
    console.log('task2', name);
})
// 調(diào)用call横浑,我們的兩個(gè)任務(wù)就會(huì)串行執(zhí)行了,
tasks.call('done')

是不是很簡(jiǎn)單屉更,下面我們學(xué)習(xí)下tapable實(shí)現(xiàn)了哪些任務(wù)執(zhí)行流程徙融,并且是如何實(shí)現(xiàn)的:

一、同步事件流

如上例子所示瑰谜,每一種hook都會(huì)有兩個(gè)方法欺冀,用于添加任務(wù)觸發(fā)任務(wù)執(zhí)行。在同步的hook中萨脑,分別對(duì)應(yīng)tapcall方法隐轩。

1. 并行

所有任務(wù)一起執(zhí)行

class SyncHook {
    constructor() {
        // 用于保存添加的任務(wù)
        this.tasks = []
    }

    tap(name, task) {
        // 注冊(cè)事件
        this.tasks.push(task)
    }

    call(...args) {
        // 把注冊(cè)的事件依次調(diào)用,無(wú)特殊處理
        this.tasks.forEach(task => task(...args))
    }
}

2. 串行可熔斷

如果其中一個(gè)task有返回值(不為undefined)渤早,就會(huì)中斷tasks的調(diào)用

class SyncBailHook {
    constructor() {
        // 用于保存添加的任務(wù)
        this.tasks = []
    }

    tap(name, task) {
        this.tasks.push(task)
    }

    call(...args) {
        for (let i = 0; i < this.tasks.length; i++) {
            const result = this.tasks[i](...args)
            // 有返回值的話(huà)职车,就會(huì)中斷調(diào)用
            if (result !== undefined) {
                break
            }
        }
    }
}

3. 串行瀑布流

task的計(jì)算結(jié)果會(huì)作為下一個(gè)task的參數(shù),以此類(lèi)推

class SyncWaterfallHook {
    constructor() {
        this.tasks = []
    }

    tap(name, task) {
        this.tasks.push(task)
    }

    call(...args) {
        const [first, ...others] = this.tasks
        const result = first(...args)
        // 上一個(gè)task的返回值會(huì)作為下一個(gè)task的函數(shù)參數(shù)
        others.reduce((result, task) => {
            return task(result)
        }, result)
    }
}

4. 串行可循環(huán)

如果task有返回值(返回值不為undefined),就會(huì)循環(huán)執(zhí)行當(dāng)前task悴灵,直到返回值為undefined才會(huì)執(zhí)行下一個(gè)task

class SyncLoopHook {
    constructor() {
        this.tasks = []
    }

    tap(name, task) {
        this.tasks.push(task)
    }

    call(...args) {
        // 當(dāng)前執(zhí)行task的index
        let currentTaskIdx = 0
        while (currentTaskIdx < this.tasks.length) {
            let task = this.tasks[currentTaskIdx]
            const result = task(...args)
            // 只有返回為undefined的時(shí)候才會(huì)執(zhí)行下一個(gè)task扛芽,否則一直執(zhí)行當(dāng)前task
            if (result === undefined) {
                currentTaskIdx++
            }
        }
    }
}

二、異步事件流

異步事件流中积瞒,綁定和觸發(fā)的方法都會(huì)有兩種實(shí)現(xiàn):

  • 使用promisetapPromise綁定川尖、promise觸發(fā)
  • promisetapAsync綁定、callAsync觸發(fā)

注意事項(xiàng):

既然我們要控制異步tasks的執(zhí)行流程茫孔,那我們必須要知道它們執(zhí)行完的時(shí)機(jī):

  • 使用promisehook空厌,任務(wù)中resolve的調(diào)用就代表異步執(zhí)行完畢了;

    // 使用promise方法的例子
    
    // 初始化異步并行的hook
    const asyncHook = new AsyncParallelHook('async')
    // 添加task
    // tapPromise需要返回一個(gè)promise
    asyncHook.tapPromise('render1', (name) => {
        return new Promise((resolve, reject) => {
            setTimeout(() => {
                console.log('render1', name);
                resolve()
            }, 1000);
        })
    
    })
    // 再添加一個(gè)task
    // tapPromise需要返回一個(gè)promise
    asyncHook.tapPromise('render2', (name) => {
        return new Promise((resolve, reject) => {
            setTimeout(() => {
                console.log('render2', name);
                resolve()
            }, 1000);
        })
    })
    // 傳入的兩個(gè)異步任務(wù)就可以串行執(zhí)行了银酬,并在執(zhí)行完畢后打印done
    asyncHook.promise().then( () => {
        console.log('done');
    })
    
  • 但在使用非promisehook時(shí)嘲更,異步任務(wù)執(zhí)行完畢的時(shí)機(jī)我們就無(wú)從獲取了。所以我們規(guī)定傳入的 task的最后一個(gè)參數(shù)參數(shù)為一個(gè)函數(shù)揩瞪,并且在異步任務(wù)執(zhí)行完畢后執(zhí)行它赋朦,這樣我們能獲取執(zhí)行完畢的時(shí)機(jī),如下例所示:

    const asyncHook = new AsyncParallelHook('async')
    // 添加task
    asyncHook.tapAsync('example', (data, cb) => {
        setTimeout(() => {
            console.log('example', name);
            // 在異步操作完成時(shí)李破,調(diào)用回調(diào)函數(shù)宠哄,表示異步任務(wù)完成
            cb()
        }, 1000);
    })
    // 添加task
    asyncHook.tapAsync('example1', (data, cb) => {
        setTimeout(() => {
            console.log('example1', name);
            // 在異步操作完成時(shí),調(diào)用回調(diào)函數(shù)嗤攻,表示異步任務(wù)完成
            cb()
        }, 1000);
    })
    // 傳入的兩個(gè)異步任務(wù)就可以串行執(zhí)行了毛嫉,并在執(zhí)行完畢后打印done
    asyncHook.callAsync('done', () => {
        console.log('done')
    })
    

1. 并行執(zhí)行

task一起執(zhí)行,所有異步事件執(zhí)行完成后妇菱,執(zhí)行最后的回調(diào)承粤。類(lèi)似promise.all

NOTE: callAsync中計(jì)數(shù)器的使用,類(lèi)似于promise.all的實(shí)現(xiàn)原理

class AsyncParallelHook {
    constructor() {
        this.tasks = []
    }

    tapAsync(name, task) {
        this.tasks.push(task)
    }

    callAsync(...args) {
        // 最后一個(gè)參數(shù)為闯团,流程結(jié)束的回調(diào)
        const finalCB = args.pop()
        let index = 0
        // 這就是每個(gè)task執(zhí)行完成時(shí)調(diào)用的回調(diào)函數(shù)
        const CB = () => {
            ++index
            // 當(dāng)這個(gè)回調(diào)函數(shù)調(diào)用的次數(shù)等于tasks的個(gè)數(shù)時(shí)辛臊,說(shuō)明任務(wù)都執(zhí)行完了
            if (index === this.tasks.length) {
                // 調(diào)用流程結(jié)束的回調(diào)函數(shù)
                finalCB()
            }
        }
        this.tasks.forEach(task => task(...args, CB))
    }

    // task是一個(gè)promise生成器
    tapPromise(name, task) {
        this.tasks.push(task)
    }
    // 使用promise.all實(shí)現(xiàn)
    promise(...args) {
        const tasks = this.tasks.map(task => task(...args))
        return Promise.all(tasks)
    }
}

2. 異步串行執(zhí)行

所有tasks串行執(zhí)行,一個(gè)tasks執(zhí)行完了在執(zhí)行下一個(gè)

NOTE:callAsync的實(shí)現(xiàn)與使用房交,類(lèi)似于generate執(zhí)行器coasync await的原理

NOTE:promise的實(shí)現(xiàn)與使用彻舰,就是面試中常見(jiàn)的 異步任務(wù)調(diào)度題 的正解。比如候味,實(shí)現(xiàn)每隔一秒打印1次刃唤,打印5次。

class AsyncSeriesHook {
    constructor() {
        this.tasks = []
    }

    tapAsync(name, task) {
        this.tasks.push(task)
    }

    callAsync(...args) {
        const finalCB = args.pop()
        let index = 0
        // 這就是每個(gè)task異步執(zhí)行完畢之后調(diào)用的回調(diào)函數(shù)
        const next = () => {
            let task = this.tasks[index++]
            if (task) {
                // task執(zhí)行完畢之后白群,會(huì)調(diào)用next尚胞,繼續(xù)執(zhí)行下一個(gè)task,形成遞歸川抡,直到任務(wù)全部執(zhí)行完
                task(...args, next)
            } else {
                // 任務(wù)完畢之后辐真,調(diào)用流程結(jié)束的回調(diào)函數(shù)
                finalCB()
            }
        }
        next()
    }

    tapPromise(name, task) {
        this.tasks.push(task)
    }

    promise(...args) {
        let [first, ...others] = this.tasks
        return others.reduce((p, n) =>{
            // then函數(shù)中返回另一個(gè)promise须尚,可以實(shí)現(xiàn)promise的串行執(zhí)行
            return p.then(() => n(...args))
        },first(...args))
    }
}

3. 串行瀑布流

異步task串行執(zhí)行,task的計(jì)算結(jié)果會(huì)作為下一個(gè)task的參數(shù)侍咱,以此類(lèi)推耐床。task執(zhí)行結(jié)果通過(guò)cb回調(diào)函數(shù)向下傳遞。

class AsyncWaterfallHook {
    constructor() {
        this.tasks = []
    }

    tapAsync(name, task) {
        this.tasks.push(task)
    }

    callAsync(...args) {
        const [first] = this.tasks
        const finalCB = args.pop()
        let index = 1
        // 這就是每個(gè)task異步執(zhí)行完畢之后調(diào)用的回調(diào)函數(shù)楔脯,其中ret為上一個(gè)task的執(zhí)行結(jié)果
        const next = (error, ret) => {
            if(error !== undefined) {
                return
            }
            let task = this.tasks[index++]
            if (task) {
                // task執(zhí)行完畢之后撩轰,會(huì)調(diào)用next,繼續(xù)執(zhí)行下一個(gè)task昧廷,形成遞歸堪嫂,直到任務(wù)全部執(zhí)行完
                task(ret, next)
            } else {
                // 任務(wù)完畢之后,調(diào)用流程結(jié)束的回調(diào)函數(shù)
                finalCB(ret)
            }
        }
        first(...args, next)
    }

    tapPromise(name, task) {
        this.tasks.push(task)
    }

    promise(...args) {
        let [first, ...others] = this.tasks
        return others.reduce((p, n) =>{
            // then函數(shù)中返回另一個(gè)promise木柬,可以實(shí)現(xiàn)promise的串行執(zhí)行
            return p.then(() => n(...args))
        }, first(...args))
    }
}

總結(jié)

學(xué)了tapable的一些hook皆串,你能擴(kuò)展到很多東西:

  • promise.all
  • co模塊
  • async await
  • 面試中的經(jīng)典手寫(xiě)代碼題:任務(wù)調(diào)度系列
  • 設(shè)計(jì)模式之監(jiān)聽(tīng)者模式
  • 設(shè)計(jì)模式之發(fā)布訂閱者模式

你都可以去實(shí)現(xiàn),用于鞏固和拓展相關(guān)知識(shí)眉枕。

我們?cè)趯W(xué)習(xí)tapable時(shí)恶复,重點(diǎn)不在于這個(gè)庫(kù)的細(xì)節(jié)和使用,而在于多個(gè)任務(wù)有可能的執(zhí)行流程以及流程的實(shí)現(xiàn)原理速挑,它們是眾多實(shí)際問(wèn)題的抽象模型谤牡,掌握了它們,你就可以在實(shí)際開(kāi)發(fā)中和面試中舉一反三姥宝,舉重若輕翅萤。

有哪些流程管理方面的面試題呢?寫(xiě)到評(píng)論區(qū)大家一起學(xué)習(xí)下!!!

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末腊满,一起剝皮案震驚了整個(gè)濱河市套么,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌糜烹,老刑警劉巖违诗,帶你破解...
    沈念sama閱讀 216,591評(píng)論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異疮蹦,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)茸炒,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,448評(píng)論 3 392
  • 文/潘曉璐 我一進(jìn)店門(mén)愕乎,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人壁公,你說(shuō)我怎么就攤上這事感论。” “怎么了紊册?”我有些...
    開(kāi)封第一講書(shū)人閱讀 162,823評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵比肄,是天一觀的道長(zhǎng)快耿。 經(jīng)常有香客問(wèn)我,道長(zhǎng)芳绩,這世上最難降的妖魔是什么掀亥? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,204評(píng)論 1 292
  • 正文 為了忘掉前任,我火速辦了婚禮妥色,結(jié)果婚禮上搪花,老公的妹妹穿的比我還像新娘。我一直安慰自己嘹害,他們只是感情好撮竿,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,228評(píng)論 6 388
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著笔呀,像睡著了一般幢踏。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上许师,一...
    開(kāi)封第一講書(shū)人閱讀 51,190評(píng)論 1 299
  • 那天房蝉,我揣著相機(jī)與錄音,去河邊找鬼枯跑。 笑死惨驶,一個(gè)胖子當(dāng)著我的面吹牛敛助,可吹牛的內(nèi)容都是我干的粗卜。 我是一名探鬼主播,決...
    沈念sama閱讀 40,078評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼纳击,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼续扔!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起焕数,我...
    開(kāi)封第一講書(shū)人閱讀 38,923評(píng)論 0 274
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤纱昧,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后堡赔,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體识脆,經(jīng)...
    沈念sama閱讀 45,334評(píng)論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,550評(píng)論 2 333
  • 正文 我和宋清朗相戀三年善已,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了灼捂。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 39,727評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡换团,死狀恐怖悉稠,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情艘包,我是刑警寧澤的猛,帶...
    沈念sama閱讀 35,428評(píng)論 5 343
  • 正文 年R本政府宣布耀盗,位于F島的核電站,受9級(jí)特大地震影響卦尊,放射性物質(zhì)發(fā)生泄漏叛拷。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,022評(píng)論 3 326
  • 文/蒙蒙 一猫牡、第九天 我趴在偏房一處隱蔽的房頂上張望胡诗。 院中可真熱鬧,春花似錦淌友、人聲如沸煌恢。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 31,672評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)瑰抵。三九已至,卻和暖如春器联,著一層夾襖步出監(jiān)牢的瞬間二汛,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 32,826評(píng)論 1 269
  • 我被黑心中介騙來(lái)泰國(guó)打工拨拓, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留肴颊,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 47,734評(píng)論 2 368
  • 正文 我出身青樓渣磷,卻偏偏與公主長(zhǎng)得像婿着,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子醋界,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,619評(píng)論 2 354

推薦閱讀更多精彩內(nèi)容

  • 弄懂js異步 講異步之前竟宋,我們必須掌握一個(gè)基礎(chǔ)知識(shí)-event-loop。 我們知道JavaScript的一大特點(diǎn)...
    DCbryant閱讀 2,710評(píng)論 0 5
  • 透徹掌握Promise的使用形纺,讀這篇就夠了 Promise的重要性我認(rèn)為我沒(méi)有必要多講丘侠,概括起來(lái)說(shuō)就是必須得掌握,...
    穿牛仔褲的蚊子閱讀 2,135評(píng)論 0 16
  • 前言 本文旨在簡(jiǎn)單講解一下javascript中的Promise對(duì)象的概念逐样,特性與簡(jiǎn)單的使用方法蜗字。并在文末會(huì)附上一...
    _暮雨清秋_閱讀 2,197評(píng)論 0 3
  • JS為什么是單線(xiàn)程的? 最初設(shè)計(jì)JS是用來(lái)在瀏覽器驗(yàn)證表單操控DOM元素的是一門(mén)腳本語(yǔ)言,如果js是多線(xiàn)程的那么兩...
    船長(zhǎng)___閱讀 2,198評(píng)論 1 15
  • 原文地址:http://es6.ruanyifeng.com/#docs/promise Promise 的含義 ...
    AI云棧閱讀 872評(píng)論 0 7