<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          webpack核心模塊tapable用法解析

          共 21779字,需瀏覽 44分鐘

           ·

          2021-05-31 13:20

          前不久寫了一篇webpack基本原理和AST用法的文章[1],本來(lái)想接著寫webpack plugin的原理的,但是發(fā)現(xiàn)webpack plugin高度依賴tapable[2]這個(gè)庫(kù),不清楚tapable而直接去看webpack plugin始終有點(diǎn)霧里看花的意思。所以就先去看了下tapable的文檔和源碼,發(fā)現(xiàn)這個(gè)庫(kù)非常有意思,是增強(qiáng)版的發(fā)布訂閱模式發(fā)布訂閱模式在源碼世界實(shí)在是太常見(jiàn)了,我們已經(jīng)在多個(gè)庫(kù)源碼里面見(jiàn)過(guò)了:

          1. redux的subscribe和dispatch[3]
          2. Node.js的EventEmitter[4]
          3. redux-saga的take和put[5]

          這些庫(kù)基本都自己實(shí)現(xiàn)了自己的發(fā)布訂閱模式,實(shí)現(xiàn)方式主要是用來(lái)滿足自己的業(yè)務(wù)需求,而tapable并沒(méi)有具體的業(yè)務(wù)邏輯,是一個(gè)專門用來(lái)實(shí)現(xiàn)事件訂閱或者他自己稱為hook(鉤子)的工具庫(kù),其根本原理還是發(fā)布訂閱模式,但是他實(shí)現(xiàn)了多種形式的發(fā)布訂閱模式,還包含了多種形式的流程控制。

          tapable暴露多個(gè)API,提供了多種流程控制方式,連使用都是比較復(fù)雜的,所以我想分兩篇文章來(lái)寫他的原理:

          1. 先看看用法,體驗(yàn)下他的多種流程控制方式
          2. 通過(guò)用法去看看源碼是怎么實(shí)現(xiàn)的

          本文就是講用法的文章,知道了他的用法,大家以后如果有自己實(shí)現(xiàn)hook或者事件監(jiān)聽的需求,可以直接拿過(guò)來(lái)用,非常強(qiáng)大!

          本文例子已經(jīng)全部上傳到GitHub,大家可以拿下來(lái)做個(gè)參考:https://github.com/dennis-jiang/Front-End-Knowledges/tree/master/Examples/Engineering/tapable-usage[6]

          tapable是什么

          tapablewebpack的核心模塊,也是webpack團(tuán)隊(duì)維護(hù)的,是webpack plugin的基本實(shí)現(xiàn)方式。他的主要功能是為使用者提供強(qiáng)大的hook機(jī)制,webpack plugin就是基于hook的。

          主要API

          下面是官方文檔中列出來(lái)的主要API,所有API的名字都是以Hook結(jié)尾的:

          const {
           SyncHook,
           SyncBailHook,
           SyncWaterfallHook,
           SyncLoopHook,
           AsyncParallelHook,
           AsyncParallelBailHook,
           AsyncSeriesHook,
           AsyncSeriesBailHook,
           AsyncSeriesWaterfallHook
           } = require("tapable");

          這些API的名字其實(shí)就解釋了他的作用,注意這些關(guān)鍵字:Sync, Async, Bail, Waterfall, Loop, Parallel, Series。下面分別來(lái)解釋下這些關(guān)鍵字:

          Sync:這是一個(gè)同步的hook

          Async:這是一個(gè)異步的hook

          BailBail在英文中的意思是保險(xiǎn),保障的意思,實(shí)現(xiàn)的效果是,當(dāng)一個(gè)hook注冊(cè)了多個(gè)回調(diào)方法,任意一個(gè)回調(diào)方法返回了不為undefined的值,就不再執(zhí)行后面的回調(diào)方法了,就起到了一個(gè)“保險(xiǎn)絲”的作用。

          WaterfallWaterfall在英語(yǔ)中是瀑布的意思,在編程世界中表示順序執(zhí)行各種任務(wù),在這里實(shí)現(xiàn)的效果是,當(dāng)一個(gè)hook注冊(cè)了多個(gè)回調(diào)方法,前一個(gè)回調(diào)執(zhí)行完了才會(huì)執(zhí)行下一個(gè)回調(diào),而前一個(gè)回調(diào)的執(zhí)行結(jié)果會(huì)作為參數(shù)傳給下一個(gè)回調(diào)函數(shù)。

          LoopLoop就是循環(huán)的意思,實(shí)現(xiàn)的效果是,當(dāng)一個(gè)hook注冊(cè)了回調(diào)方法,如果這個(gè)回調(diào)方法返回了true就重復(fù)循環(huán)這個(gè)回調(diào),只有當(dāng)這個(gè)回調(diào)返回undefined才執(zhí)行下一個(gè)回調(diào)。

          ParallelParallel是并行的意思,有點(diǎn)類似于Promise.all,就是當(dāng)一個(gè)hook注冊(cè)了多個(gè)回調(diào)方法,這些回調(diào)同時(shí)開始并行執(zhí)行。

          SeriesSeries就是串行的意思,就是當(dāng)一個(gè)hook注冊(cè)了多個(gè)回調(diào)方法,前一個(gè)執(zhí)行完了才會(huì)執(zhí)行下一個(gè)。

          ParallelSeries的概念只存在于異步的hook中,因?yàn)橥?code style="font-size: 14px;overflow-wrap: break-word;padding: 2px 4px;border-radius: 4px;margin-right: 2px;margin-left: 2px;color: rgb(30, 107, 184);background-color: rgba(27, 31, 35, 0.05);font-family: "Operator Mono", Consolas, Monaco, Menlo, monospace;word-break: break-all;">hook全部是串行的。

          下面我們分別來(lái)介紹下每個(gè)API的用法和效果。

          同步API

          同步API就是這幾個(gè):

          const {
           SyncHook,
           SyncBailHook,
           SyncWaterfallHook,
           SyncLoopHook,
           } = require("tapable");

          前面說(shuō)了,同步API全部是串行的,所以這幾個(gè)的區(qū)別就在流程控制上。

          SyncHook

          SyncHook是一個(gè)最基礎(chǔ)的hook,其使用方法和效果接近我們經(jīng)常使用的發(fā)布訂閱模式,注意tapable導(dǎo)出的所有hook都是類,基本用法是這樣的:

          const hook = new SyncHook(["arg1""arg2""arg3"]);

          因?yàn)?code style="font-size: 14px;overflow-wrap: break-word;padding: 2px 4px;border-radius: 4px;margin-right: 2px;margin-left: 2px;color: rgb(30, 107, 184);background-color: rgba(27, 31, 35, 0.05);font-family: "Operator Mono", Consolas, Monaco, Menlo, monospace;word-break: break-all;">SyncHook是一個(gè)類,所以使用new來(lái)生成一個(gè)實(shí)例,構(gòu)造函數(shù)接收的參數(shù)是一個(gè)數(shù)組["arg1", "arg2", "arg3"],這個(gè)數(shù)組有三項(xiàng),表示生成的這個(gè)實(shí)例注冊(cè)回調(diào)的時(shí)候接收三個(gè)參數(shù)。實(shí)例hook主要有兩個(gè)實(shí)例方法:

          1. tap:就是注冊(cè)事件回調(diào)的方法。
          2. call:就是觸發(fā)事件,執(zhí)行回調(diào)的方法。

          下面我們擴(kuò)展下官方文檔中小汽車加速的例子來(lái)說(shuō)明下具體用法:

          const { SyncHook } = require("tapable");

          // 實(shí)例化一個(gè)加速的hook
          const accelerate = new SyncHook(["newSpeed"]);

          // 注冊(cè)第一個(gè)回調(diào),加速時(shí)記錄下當(dāng)前速度
          accelerate.tap("LoggerPlugin", (newSpeed) =>
            console.log("LoggerPlugin"`加速到 ${newSpeed}`)
          );

          // 再注冊(cè)一個(gè)回調(diào),用來(lái)檢測(cè)是否超速
          accelerate.tap("OverspeedPlugin", (newSpeed) => {
            if (newSpeed > 120) {
              console.log("OverspeedPlugin""您已超速!!");
            }
          });

          // 再注冊(cè)一個(gè)回調(diào),用來(lái)檢測(cè)速度是否快到損壞車子了
          accelerate.tap("DamagePlugin", (newSpeed) => {
            if (newSpeed > 300) {
              console.log("DamagePlugin""速度實(shí)在太快,車子快散架了。。。");
            }
          });

          // 觸發(fā)一下加速事件,看看效果吧
          accelerate.call(500);

          然后運(yùn)行下看看吧,當(dāng)加速事件出現(xiàn)的時(shí)候,會(huì)依次執(zhí)行這三個(gè)回調(diào):

          image-20210309160302799

          上面這個(gè)例子主要就是用了tapcall這兩個(gè)實(shí)例方法,其中tap接收兩個(gè)參數(shù),第一個(gè)是個(gè)字符串,并沒(méi)有實(shí)際用處,僅僅是一個(gè)注釋的作用,第二個(gè)參數(shù)就是一個(gè)回調(diào)函數(shù),用來(lái)執(zhí)行事件觸發(fā)時(shí)的具體邏輯。

          accelerate.tap("LoggerPlugin", (newSpeed) =>
            console.log("LoggerPlugin"`加速到 ${newSpeed}`)
          );

          上述這種寫法其實(shí)與webpack官方文檔中對(duì)于plugin的介紹非常像了[7],因?yàn)?code style="font-size: 14px;overflow-wrap: break-word;padding: 2px 4px;border-radius: 4px;margin-right: 2px;margin-left: 2px;color: rgb(30, 107, 184);background-color: rgba(27, 31, 35, 0.05);font-family: "Operator Mono", Consolas, Monaco, Menlo, monospace;word-break: break-all;">webpack的plguin就是用tapable實(shí)現(xiàn)的,第一個(gè)參數(shù)一般就是plugin的名字:

          image-20210309154641835

          call就是簡(jiǎn)單的觸發(fā)這個(gè)事件,在webpackplguin中一般不需要開發(fā)者去觸發(fā)事件,而是webpack自己在不同階段會(huì)觸發(fā)不同的事件,比如beforeRun, run等等,plguin開發(fā)者更多的會(huì)關(guān)注這些事件出現(xiàn)時(shí)應(yīng)該進(jìn)行什么操作,也就是在這些事件上注冊(cè)自己的回調(diào)。

          SyncBailHook

          上面的SyncHook其實(shí)就是一個(gè)簡(jiǎn)單的發(fā)布訂閱模式SyncBailHook就是在這個(gè)基礎(chǔ)上加了一點(diǎn)流程控制,前面我們說(shuō)過(guò)了,Bail就是個(gè)保險(xiǎn),實(shí)現(xiàn)的效果是,前面一個(gè)回調(diào)返回一個(gè)不為undefined的值,就中斷這個(gè)流程。比如我們現(xiàn)在將前面這個(gè)例子的SyncHook換成SyncBailHook,然后在檢測(cè)超速的這個(gè)插件里面加點(diǎn)邏輯,當(dāng)它超速了就返回錯(cuò)誤,后面的DamagePlugin就不會(huì)執(zhí)行了:

          const { SyncBailHook } = require("tapable");    // 使用的是SyncBailHook

          // 實(shí)例化一個(gè)加速的hook
          const accelerate = new SyncBailHook(["newSpeed"]);

          accelerate.tap("LoggerPlugin", (newSpeed) =>
            console.log("LoggerPlugin"`加速到 ${newSpeed}`)
          );

          // 再注冊(cè)一個(gè)回調(diào),用來(lái)檢測(cè)是否超速
          // 如果超速就返回一個(gè)錯(cuò)誤
          accelerate.tap("OverspeedPlugin", (newSpeed) => {
            if (newSpeed > 120) {
              console.log("OverspeedPlugin""您已超速!!");

              return new Error('您已超速!!');
            }
          });

          accelerate.tap("DamagePlugin", (newSpeed) => {
            if (newSpeed > 300) {
              console.log("DamagePlugin""速度實(shí)在太快,車子快散架了。。。");
            }
          });

          accelerate.call(500);

          然后再運(yùn)行下看看:

          image-20210309161001682

          可以看到由于OverspeedPlugin返回了一個(gè)不為undefined的值,DamagePlugin被阻斷,沒(méi)有運(yùn)行了。

          SyncWaterfallHook

          SyncWaterfallHook也是在SyncHook的基礎(chǔ)上加了點(diǎn)流程控制,前面說(shuō)了,Waterfall實(shí)現(xiàn)的效果是將上一個(gè)回調(diào)的返回值作為參數(shù)傳給下一個(gè)回調(diào)。所以通過(guò)call傳入的參數(shù)只會(huì)傳遞給第一個(gè)回調(diào)函數(shù),后面的回調(diào)接受都是上一個(gè)回調(diào)的返回值,最后一個(gè)回調(diào)的返回值會(huì)作為call的返回值返回給最外層:

          const { SyncWaterfallHook } = require("tapable");

          const accelerate = new SyncWaterfallHook(["newSpeed"]);

          accelerate.tap("LoggerPlugin", (newSpeed) => {
            console.log("LoggerPlugin"`加速到 ${newSpeed}`);

            return "LoggerPlugin";
          });

          accelerate.tap("Plugin2", (data) => {
            console.log(`上一個(gè)插件是: ${data}`);

            return "Plugin2";
          });

          accelerate.tap("Plugin3", (data) => {
            console.log(`上一個(gè)插件是: ${data}`);

            return "Plugin3";
          });

          const lastPlugin = accelerate.call(100);

          console.log(`最后一個(gè)插件是:${lastPlugin}`);

          然后看下運(yùn)行效果吧:

          image-20210309162008465

          SyncLoopHook

          SyncLoopHook是在SyncHook的基礎(chǔ)上添加了循環(huán)的邏輯,也就是如果一個(gè)插件返回true就會(huì)一直執(zhí)行這個(gè)插件,直到他返回undefined才會(huì)執(zhí)行下一個(gè)插件:

          const { SyncLoopHook } = require("tapable");

          const accelerate = new SyncLoopHook(["newSpeed"]);

          accelerate.tap("LoopPlugin", (newSpeed) => {
            console.log("LoopPlugin"`循環(huán)加速到 ${newSpeed}`);

            return new Date().getTime() % 5 !== 0 ? true : undefined;
          });

          accelerate.tap("LastPlugin", (newSpeed) => {
            console.log("循環(huán)加速總算結(jié)束了");
          });

          accelerate.call(100);

          執(zhí)行效果如下:

          image-20210309163514680

          異步API

          所謂異步API是相對(duì)前面的同步API來(lái)說(shuō)的,前面的同步API的所有回調(diào)都是按照順序同步執(zhí)行的,每個(gè)回調(diào)內(nèi)部也全部是同步代碼。但是實(shí)際項(xiàng)目中,可能需要回調(diào)里面處理異步情況,也可能希望多個(gè)回調(diào)可以同時(shí)并行執(zhí)行,也就是Parallel。這些需求就需要用到異步API了,主要的異步API就是這些:

          const {
           AsyncParallelHook,
           AsyncParallelBailHook,
           AsyncSeriesHook,
           AsyncSeriesBailHook,
           AsyncSeriesWaterfallHook
           } = require("tapable");

          既然涉及到了異步,那肯定還需要異步的處理方式,tapable支持回調(diào)函數(shù)和Promise兩種異步的處理方式。所以這些異步API除了用前面的tap來(lái)注冊(cè)回調(diào)外,還有兩個(gè)注冊(cè)回調(diào)的方法:tapAsynctapPromise,對(duì)應(yīng)的觸發(fā)事件的方法為callAsyncpromise。下面分別來(lái)看下每個(gè)API吧:

          AsyncParallelHook

          AsyncParallelHook從前面介紹的命名規(guī)則可以看出,他是一個(gè)異步并行執(zhí)行的Hook,我們先用tapAsync的方式來(lái)看下怎么用吧。

          tapAsync和callAsync

          還是那個(gè)小汽車加速的例子,只不過(guò)這個(gè)小汽車加速?zèng)]那么快了,需要一秒才能加速完成,然后我們?cè)?秒的時(shí)候分別檢測(cè)是否超速和是否損壞,為了看出并行的效果,我們記錄下整個(gè)過(guò)程從開始到結(jié)束的時(shí)間:

          const { AsyncParallelHook } = require("tapable");

          const accelerate = new AsyncParallelHook(["newSpeed"]);

          console.time("total time"); // 記錄起始時(shí)間

          // 注意注冊(cè)異步事件需要使用tapAsync
          // 接收的最后一個(gè)參數(shù)是done,調(diào)用他來(lái)表示當(dāng)前任務(wù)執(zhí)行完畢
          accelerate.tapAsync("LoggerPlugin", (newSpeed, done) => {
            // 1秒后加速才完成
            setTimeout(() => {
              console.log("LoggerPlugin"`加速到 ${newSpeed}`);

              done();
            }, 1000);
          });

          accelerate.tapAsync("OverspeedPlugin", (newSpeed, done) => {
            // 2秒后檢測(cè)是否超速
            setTimeout(() => {
              if (newSpeed > 120) {
                console.log("OverspeedPlugin""您已超速!!");
              }
              done();
            }, 2000);
          });

          accelerate.tapAsync("DamagePlugin", (newSpeed, done) => {
            // 2秒后檢測(cè)是否損壞
            setTimeout(() => {
              if (newSpeed > 300) {
                console.log("DamagePlugin""速度實(shí)在太快,車子快散架了。。。");
              }

              done();
            }, 2000);
          });

          accelerate.callAsync(500, () => {
            console.log("任務(wù)全部完成");
            console.timeEnd("total time"); // 記錄總共耗時(shí)
          });

          上面代碼需要注意的是,注冊(cè)回調(diào)要使用tapAsync,而且回調(diào)函數(shù)里面最后一個(gè)參數(shù)會(huì)自動(dòng)傳入done,你可以調(diào)用他來(lái)通知tapable當(dāng)前任務(wù)已經(jīng)完成。觸發(fā)任務(wù)需要使用callAsync,他最后也接收一個(gè)函數(shù),可以用來(lái)處理所有任務(wù)都完成后需要執(zhí)行的操作。所以上面的運(yùn)行結(jié)果就是:

          image-20210309171527773

          從這個(gè)結(jié)果可以看出,最終消耗的時(shí)間大概是2秒,也就是三個(gè)任務(wù)中最長(zhǎng)的單個(gè)任務(wù)耗時(shí),而不是三個(gè)任務(wù)耗時(shí)的總額,這就實(shí)現(xiàn)了Parallel并行的效果。

          tapPromise和promise

          現(xiàn)在都流行Promise,所以tapable也是支持的,執(zhí)行效果是一樣的,只是寫法不一樣而已。要用tapPromise,需要注冊(cè)的回調(diào)返回一個(gè)promise,同時(shí)觸發(fā)事件也需要用promise,任務(wù)運(yùn)行完執(zhí)行的處理可以直接使用then,所以上述代碼改為:

          const { AsyncParallelHook } = require("tapable");

          const accelerate = new AsyncParallelHook(["newSpeed"]);

          console.time("total time"); // 記錄起始時(shí)間

          // 注意注冊(cè)異步事件需要使用tapPromise
          // 回調(diào)函數(shù)要返回一個(gè)promise
          accelerate.tapPromise("LoggerPlugin", (newSpeed) => {
            return new Promise((resolve) => {
              // 1秒后加速才完成
              setTimeout(() => {
                console.log("LoggerPlugin"`加速到 ${newSpeed}`);

                resolve();
              }, 1000);
            });
          });

          accelerate.tapPromise("OverspeedPlugin", (newSpeed) => {
            return new Promise((resolve) => {
              // 2秒后檢測(cè)是否超速
              setTimeout(() => {
                if (newSpeed > 120) {
                  console.log("OverspeedPlugin""您已超速!!");
                }
                resolve();
              }, 2000);
            });
          });

          accelerate.tapPromise("DamagePlugin", (newSpeed) => {
            return new Promise((resolve) => {
              // 2秒后檢測(cè)是否損壞
              setTimeout(() => {
                if (newSpeed > 300) {
                  console.log("DamagePlugin""速度實(shí)在太快,車子快散架了。。。");
                }

                resolve();
              }, 2000);
            });
          });

          // 觸發(fā)事件使用promise,直接用then處理最后的結(jié)果
          accelerate.promise(500).then(() => {
            console.log("任務(wù)全部完成");
            console.timeEnd("total time"); // 記錄總共耗時(shí)
          });

          這段代碼的邏輯和運(yùn)行結(jié)果和上面那個(gè)是一樣的,只是寫法不一樣:

          image-20210309172537951

          tapAsync和tapPromise混用

          既然tapable支持這兩種異步寫法,那這兩種寫法可以混用嗎?我們來(lái)試試吧:

          const { AsyncParallelHook } = require("tapable");

          const accelerate = new AsyncParallelHook(["newSpeed"]);

          console.time("total time"); // 記錄起始時(shí)間

          // 來(lái)一個(gè)promise寫法
          accelerate.tapPromise("LoggerPlugin", (newSpeed) => {
            return new Promise((resolve) => {
              // 1秒后加速才完成
              setTimeout(() => {
                console.log("LoggerPlugin"`加速到 ${newSpeed}`);

                resolve();
              }, 1000);
            });
          });

          // 再來(lái)一個(gè)async寫法
          accelerate.tapAsync("OverspeedPlugin", (newSpeed, done) => {
            // 2秒后檢測(cè)是否超速
            setTimeout(() => {
              if (newSpeed > 120) {
                console.log("OverspeedPlugin""您已超速!!");
              }
              done();
            }, 2000);
          });

          // 使用promise觸發(fā)事件
          // accelerate.promise(500).then(() => {
          //   console.log("任務(wù)全部完成");
          //   console.timeEnd("total time"); // 記錄總共耗時(shí)
          // });

          // 使用callAsync觸發(fā)事件
          accelerate.callAsync(500, () => {
            console.log("任務(wù)全部完成");
            console.timeEnd("total time"); // 記錄總共耗時(shí)
          });

          這段代碼無(wú)論我是使用promise觸發(fā)事件還是callAsync觸發(fā)運(yùn)行的結(jié)果都是一樣的,所以tapable內(nèi)部應(yīng)該是做了兼容轉(zhuǎn)換的,兩種寫法可以混用:

          image-20210309173217034

          由于tapAsynctapPromise只是寫法上的不一樣,我后面的例子就全部用tapAsync了。

          AsyncParallelBailHook

          前面已經(jīng)看了SyncBailHook,知道帶Bail的功能就是當(dāng)一個(gè)任務(wù)返回不為undefined的時(shí)候,阻斷后面任務(wù)的執(zhí)行。但是由于Parallel任務(wù)都是同時(shí)開始的,阻斷是阻斷不了了,實(shí)際效果是如果有一個(gè)任務(wù)返回了不為undefined的值,最終的回調(diào)會(huì)立即執(zhí)行,并且獲取Bail任務(wù)的返回值。我們將上面三個(gè)任務(wù)執(zhí)行時(shí)間錯(cuò)開,分別為1秒,2秒,3秒,然后在2秒的任務(wù)觸發(fā)Bail就能看到效果了:

          const { AsyncParallelBailHook } = require("tapable");

          const accelerate = new AsyncParallelBailHook(["newSpeed"]);

          console.time("total time"); // 記錄起始時(shí)間

          accelerate.tapAsync("LoggerPlugin", (newSpeed, done) => {
            // 1秒后加速才完成
            setTimeout(() => {
              console.log("LoggerPlugin"`加速到 ${newSpeed}`);

              done();
            }, 1000);
          });

          accelerate.tapAsync("OverspeedPlugin", (newSpeed, done) => {
            // 2秒后檢測(cè)是否超速
            setTimeout(() => {
              if (newSpeed > 120) {
                console.log("OverspeedPlugin""您已超速!!");
              }

              // 這個(gè)任務(wù)的done返回一個(gè)錯(cuò)誤
              // 注意第一個(gè)參數(shù)是node回調(diào)約定俗成的錯(cuò)誤
              // 第二個(gè)參數(shù)才是Bail的返回值
              done(nullnew Error("您已超速!!"));
            }, 2000);
          });

          accelerate.tapAsync("DamagePlugin", (newSpeed, done) => {
            // 3秒后檢測(cè)是否損壞
            setTimeout(() => {
              if (newSpeed > 300) {
                console.log("DamagePlugin""速度實(shí)在太快,車子快散架了。。。");
              }

              done();
            }, 3000);
          });

          accelerate.callAsync(500, (error, data) => {
            if (data) {
              console.log("任務(wù)執(zhí)行出錯(cuò):", data);
            } else {
              console.log("任務(wù)全部完成");
            }
            console.timeEnd("total time"); // 記錄總共耗時(shí)
          });

          可以看到執(zhí)行到任務(wù)2時(shí),由于他返回了一個(gè)錯(cuò)誤,所以最終的回調(diào)會(huì)立即執(zhí)行,但是由于任務(wù)3之前已經(jīng)同步開始了,所以他自己仍然會(huì)運(yùn)行完,只是已經(jīng)不影響最終結(jié)果了:

          image-20210311142451224

          AsyncSeriesHook

          AsyncSeriesHook是異步串行hook,如果有多個(gè)任務(wù),這多個(gè)任務(wù)之間是串行的,但是任務(wù)本身卻可能是異步的,下一個(gè)任務(wù)必須等上一個(gè)任務(wù)done了才能開始:

          const { AsyncSeriesHook } = require("tapable");

          const accelerate = new AsyncSeriesHook(["newSpeed"]);

          console.time("total time"); // 記錄起始時(shí)間

          accelerate.tapAsync("LoggerPlugin", (newSpeed, done) => {
            // 1秒后加速才完成
            setTimeout(() => {
              console.log("LoggerPlugin"`加速到 ${newSpeed}`);

              done();
            }, 1000);
          });

          accelerate.tapAsync("OverspeedPlugin", (newSpeed, done) => {
            // 2秒后檢測(cè)是否超速
            setTimeout(() => {
              if (newSpeed > 120) {
                console.log("OverspeedPlugin""您已超速!!");
              }
              done();
            }, 2000);
          });

          accelerate.tapAsync("DamagePlugin", (newSpeed, done) => {
            // 2秒后檢測(cè)是否損壞
            setTimeout(() => {
              if (newSpeed > 300) {
                console.log("DamagePlugin""速度實(shí)在太快,車子快散架了。。。");
              }

              done();
            }, 2000);
          });

          accelerate.callAsync(500, () => {
            console.log("任務(wù)全部完成");
            console.timeEnd("total time"); // 記錄總共耗時(shí)
          });

          每個(gè)任務(wù)代碼跟AsyncParallelHook是一樣的,只是使用的Hook不一樣,而最終效果的區(qū)別是:AsyncParallelHook所有任務(wù)同時(shí)開始,所以最終總耗時(shí)就是耗時(shí)最長(zhǎng)的那個(gè)任務(wù)的耗時(shí);AsyncSeriesHook的任務(wù)串行執(zhí)行,下一個(gè)任務(wù)要等上一個(gè)任務(wù)完成了才能開始,所以最終總耗時(shí)是所有任務(wù)耗時(shí)的總和,上面這個(gè)例子就是1 + 2 + 2,也就是5秒:

          image-20210311144738884

          AsyncSeriesBailHook

          AsyncSeriesBailHook就是在AsyncSeriesHook的基礎(chǔ)上加上了Bail的邏輯,也就是中間任何一個(gè)任務(wù)返回不為undefined的值,終止執(zhí)行,直接執(zhí)行最后的回調(diào),并且將這個(gè)返回值傳給最終的回調(diào):

          const { AsyncSeriesBailHook } = require("tapable");

          const accelerate = new AsyncSeriesBailHook(["newSpeed"]);

          console.time("total time"); // 記錄起始時(shí)間

          accelerate.tapAsync("LoggerPlugin", (newSpeed, done) => {
            // 1秒后加速才完成
            setTimeout(() => {
              console.log("LoggerPlugin"`加速到 ${newSpeed}`);

              done();
            }, 1000);
          });

          accelerate.tapAsync("OverspeedPlugin", (newSpeed, done) => {
            // 2秒后檢測(cè)是否超速
            setTimeout(() => {
              if (newSpeed > 120) {
                console.log("OverspeedPlugin""您已超速!!");
              }

              // 這個(gè)任務(wù)的done返回一個(gè)錯(cuò)誤
              // 注意第一個(gè)參數(shù)是node回調(diào)約定俗成的錯(cuò)誤
              // 第二個(gè)參數(shù)才是Bail的返回值
              done(nullnew Error("您已超速!!"));
            }, 2000);
          });

          accelerate.tapAsync("DamagePlugin", (newSpeed, done) => {
            // 2秒后檢測(cè)是否損壞
            setTimeout(() => {
              if (newSpeed > 300) {
                console.log("DamagePlugin""速度實(shí)在太快,車子快散架了。。。");
              }

              done();
            }, 2000);
          });

          accelerate.callAsync(500, (error, data) => {
            if (data) {
              console.log("任務(wù)執(zhí)行出錯(cuò):", data);
            } else {
              console.log("任務(wù)全部完成");
            }
            console.timeEnd("total time"); // 記錄總共耗時(shí)
          });

          這個(gè)執(zhí)行結(jié)果跟AsyncParallelBailHook的區(qū)別就是AsyncSeriesBailHook被阻斷后,后面的任務(wù)由于還沒(méi)開始,所以可以被完全阻斷,而AsyncParallelBailHook后面的任務(wù)由于已經(jīng)開始了,所以還會(huì)繼續(xù)執(zhí)行,只是結(jié)果已經(jīng)不關(guān)心了。

          image-20210311145241190

          AsyncSeriesWaterfallHook

          Waterfall的作用是將前一個(gè)任務(wù)的結(jié)果傳給下一個(gè)任務(wù),其他的跟AsyncSeriesHook一樣的,直接來(lái)看代碼吧:

          const { AsyncSeriesWaterfallHook } = require("tapable");

          const accelerate = new AsyncSeriesWaterfallHook(["newSpeed"]);

          console.time("total time"); // 記錄起始時(shí)間

          accelerate.tapAsync("LoggerPlugin", (newSpeed, done) => {
            // 1秒后加速才完成
            setTimeout(() => {
              console.log("LoggerPlugin"`加速到 ${newSpeed}`);

              // 注意done的第一個(gè)參數(shù)會(huì)被當(dāng)做error
              // 第二個(gè)參數(shù)才是傳遞給后面任務(wù)的參數(shù)
              done(null"LoggerPlugin");
            }, 1000);
          });

          accelerate.tapAsync("Plugin2", (data, done) => {
            setTimeout(() => {
              console.log(`上一個(gè)插件是: ${data}`);

              done(null"Plugin2");
            }, 2000);
          });

          accelerate.tapAsync("Plugin3", (data, done) => {
            setTimeout(() => {
              console.log(`上一個(gè)插件是: ${data}`);

              done(null"Plugin3");
            }, 2000);
          });

          accelerate.callAsync(500, (error, data) => {
            console.log("最后一個(gè)插件是:", data);
            console.timeEnd("total time"); // 記錄總共耗時(shí)
          });

          運(yùn)行效果如下:

          image-20210311150510851

          總結(jié)

          本文例子已經(jīng)全部上傳到GitHub,大家可以拿下來(lái)做個(gè)參考:https://github.com/dennis-jiang/Front-End-Knowledges/tree/master/Examples/Engineering/tapable-usage[8]

          1. tapablewebpack實(shí)現(xiàn)plugin的核心庫(kù),他為webpack提供了多種事件處理和流程控制的Hook
          2. 這些Hook主要有同步(Sync)和異步(Async)兩種,同時(shí)還提供了阻斷(Bail),瀑布(Waterfall),循環(huán)(Loop)等流程控制,對(duì)于異步流程還提供了并行(Paralle)和串行(Series)兩種控制方式。
          3. tapable其核心原理還是事件的發(fā)布訂閱模式,他使用tap來(lái)注冊(cè)事件,使用call來(lái)觸發(fā)事件。
          4. 異步hook支持兩種寫法:回調(diào)和Promise,注冊(cè)和觸發(fā)事件分別使用tapAsync/callAsynctapPromise/promise
          5. 異步hook使用回調(diào)寫法的時(shí)候要注意,回調(diào)函數(shù)的第一個(gè)參數(shù)默認(rèn)是錯(cuò)誤,第二個(gè)參數(shù)才是向外傳遞的數(shù)據(jù),這也符合node回調(diào)的風(fēng)格。

          這篇文章主要講述了tapable的用法,后面我會(huì)寫一篇文章來(lái)分析他的源碼,點(diǎn)個(gè)關(guān)注不迷路,哈哈~


          覺(jué)得博主寫得還可以的話,不要忘了分享、點(diǎn)贊、在看三連哦~

          長(zhǎng)按下方圖片,關(guān)注進(jìn)擊的大前端,獲取更多的優(yōu)質(zhì)原創(chuàng)文章~ 

          參考資料

          [1]

          webpack基本原理和AST用法的文章: https://juejin.cn/post/6930877602840182791

          [2]

          tapable: https://github.com/webpack/tapable

          [3]

          reduxsubscribedispatch: https://juejin.cn/post/6845166891682512909

          [4]

          Node.jsEventEmitter: https://juejin.cn/post/6844904101331877895

          [5]

          redux-sagatakeput: https://juejin.cn/post/6885223002703822855

          [6]

          https://github.com/dennis-jiang/Front-End-Knowledges/tree/master/Examples/Engineering/tapable-usage: https://github.com/dennis-jiang/Front-End-Knowledges/tree/master/Examples/Engineering/tapable-usage

          [7]

          webpack官方文檔中對(duì)于plugin的介紹非常像了: https://www.webpackjs.com/concepts/plugins/#%E5%89%96%E6%9E%90

          [8]

          https://github.com/dennis-jiang/Front-End-Knowledges/tree/master/Examples/Engineering/tapable-usage: https://github.com/dennis-jiang/Front-End-Knowledges/tree/master/Examples/Engineering/tapable-usage

          [9]

          進(jìn)擊的大前端: https://test-dennis.oss-cn-hangzhou.aliyuncs.com/QRCode/QR430.jpg

          [10]

          https://juejin.im/post/5e3ffc85518825494e2772fd: https://juejin.im/post/5e3ffc85518825494e2772fd

          [11]

          https://github.com/dennis-jiang/Front-End-Knowledges: https://github.com/dennis-jiang/Front-End-Knowledges


          瀏覽 46
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評(píng)論
          圖片
          表情
          推薦
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  亚洲无码视频免费在线观看 | 亚洲国产内射 | 日韩黄色电影在线观看 | 国产传媒一区二区三区 | 超碰成人人人操 |