溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊(cè)×
其他方式登錄
點(diǎn)擊 登錄注冊(cè) 即表示同意《億速云用戶服務(wù)條款》

淺談Webpack核心模塊tapable解析

發(fā)布時(shí)間:2020-09-02 13:01:03 來(lái)源:腳本之家 閱讀:147 作者:pandashen 欄目:web開(kāi)發(fā)

本文介紹了Webpack核心模塊tapable,分享給大家,具體如下:

淺談Webpack核心模塊tapable解析

前言

Webpack 是一個(gè)現(xiàn)代 JavaScript 應(yīng)用程序的靜態(tài)模塊打包器,是對(duì)前端項(xiàng)目實(shí)現(xiàn)自動(dòng)化和優(yōu)化必不可少的工具,Webpack 的 loader (加載器)和 plugin (插件)是由 Webpack 開(kāi)發(fā)者和社區(qū)開(kāi)發(fā)者共同貢獻(xiàn)的,而目前又沒(méi)有比較系統(tǒng)的開(kāi)發(fā)文檔,想寫加載器和插件必須要懂 Webpack 的原理,即看懂 Webpack 的源碼, tapable 則是 Webpack 依賴的核心庫(kù),可以說(shuō)不懂 tapable 就看不懂 Webpack 源碼,所以本篇會(huì)對(duì) tapable 提供的類進(jìn)行解析和模擬。

tapable 介紹

Webpack 本質(zhì)上是一種事件流的機(jī)制,它的工作流程就是將各個(gè)插件串聯(lián)起來(lái),而實(shí)現(xiàn)這一切的核心就是 tapable ,Webpack 中最核心的,負(fù)責(zé)編譯的 Compiler 和負(fù)責(zé)創(chuàng)建 bundles 的 Compilation 都是 tapable 構(gòu)造函數(shù)的實(shí)例。

打開(kāi) Webpack 4.0 的源碼中一定會(huì)看到下面這些以 Sync 、 Async 開(kāi)頭,以 Hook 結(jié)尾的方法,這些都是 tapable 核心庫(kù)的類,為我們提供不同的事件流執(zhí)行機(jī)制,我們稱為 “鉤子”。

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

上面的實(shí)現(xiàn)事件流機(jī)制的 “鉤子” 大方向可以分為兩個(gè)類別,“同步” 和 “異步”,“異步” 又分為兩個(gè)類別,“并行” 和 “串行”,而 “同步” 的鉤子都是串行的。

Sync 類型的鉤子

1、SyncHook

SyncHook 為串行同步執(zhí)行,不關(guān)心事件處理函數(shù)的返回值,在觸發(fā)事件之后,會(huì)按照事件注冊(cè)的先后順序執(zhí)行所有的事件處理函數(shù)。

// SyncHook 鉤子的使用
const { SyncHook } = require("tapable");

// 創(chuàng)建實(shí)例
let syncHook = new SyncHook(["name", "age"]);

// 注冊(cè)事件
syncHook.tap("1", (name, age) => console.log("1", name, age));
syncHook.tap("2", (name, age) => console.log("2", name, age));
syncHook.tap("3", (name, age) => console.log("3", name, age));

// 觸發(fā)事件,讓監(jiān)聽(tīng)函數(shù)執(zhí)行
syncHook.call("panda", 18);

// 1 panda 18
// 2 panda 18
// 3 panda 18

在 tapable 解構(gòu)的 SyncHook 是一個(gè)類,注冊(cè)事件需先創(chuàng)建實(shí)例,創(chuàng)建實(shí)例時(shí)支持傳入一個(gè)數(shù)組,數(shù)組內(nèi)存儲(chǔ)事件觸發(fā)時(shí)傳入的參數(shù),實(shí)例的 tap 方法用于注冊(cè)事件,支持傳入兩個(gè)參數(shù),第一個(gè)參數(shù)為事件名稱,在 Webpack 中一般用于存儲(chǔ)事件對(duì)應(yīng)的插件名稱(名字隨意,只是起到注釋作用), 第二個(gè)參數(shù)為事件處理函數(shù),函數(shù)參數(shù)為執(zhí)行 call 方法觸發(fā)事件時(shí)所傳入的參數(shù)的形參。

// 模擬 SyncHook 類
class SyncHook {
  constructor(args) {
    this.args = args;
    this.tasks = [];
  }
  tap(name, task) {
    this.tasks.push(task);
  }
  call(...args) {
    // 也可在參數(shù)不足時(shí)拋出異常
    if (args.length < this.args.length) throw new Error("參數(shù)不足");

    // 傳入?yún)?shù)嚴(yán)格對(duì)應(yīng)創(chuàng)建實(shí)例傳入數(shù)組中的規(guī)定的參數(shù),執(zhí)行時(shí)多余的參數(shù)為 undefined
    args = args.slice(0, this.args.length);

    // 依次執(zhí)行事件處理函數(shù)
    this.tasks.forEach(task => task(...args));
  }
}

tasks 數(shù)組用于存儲(chǔ)事件處理函數(shù), call 方法調(diào)用時(shí)傳入?yún)?shù)超過(guò)創(chuàng)建 SyncHook 實(shí)例傳入的數(shù)組長(zhǎng)度時(shí),多余參數(shù)可處理為 undefined ,也可在參數(shù)不足時(shí)拋出異常,不靈活,后面的例子中就不再這樣寫了。

2、SyncBailHook

SyncBailHook 同樣為串行同步執(zhí)行,如果事件處理函數(shù)執(zhí)行時(shí)有一個(gè)返回值不為空(即返回值為 undefined ),則跳過(guò)剩下未執(zhí)行的事件處理函數(shù)(如類的名字,意義在于保險(xiǎn))。

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

// 創(chuàng)建實(shí)例
let syncBailHook = new SyncBailHook(["name", "age"]);

// 注冊(cè)事件
syncBailHook.tap("1", (name, age) => console.log("1", name, age));

syncBailHook.tap("2", (name, age) => {
  console.log("2", name, age);
  return "2";
});

syncBailHook.tap("3", (name, age) => console.log("3", name, age));

// 觸發(fā)事件,讓監(jiān)聽(tīng)函數(shù)執(zhí)行
syncBailHook.call("panda", 18);

// 1 panda 18
// 2 panda 18

通過(guò)上面的用法可以看出, SyncHook 和 SyncBailHook 在邏輯上只是 call 方法不同,導(dǎo)致事件的執(zhí)行機(jī)制不同,對(duì)于后面其他的 “鉤子”,也是 call 的區(qū)別,接下來(lái)實(shí)現(xiàn) SyncBailHook 類。

// 模擬 SyncBailHook 類
class SyncBailHook {
  constructor(args) {
    this.args = args;
    this.tasks = [];
  }
  tap(name, task) {
    this.tasks.push(task);
  }
  call(...args) {
    // 傳入?yún)?shù)嚴(yán)格對(duì)應(yīng)創(chuàng)建實(shí)例傳入數(shù)組中的規(guī)定的參數(shù),執(zhí)行時(shí)多余的參數(shù)為 undefined
    args = args.slice(0, this.args.length);

    // 依次執(zhí)行事件處理函數(shù),如果返回值不為空,則停止向下執(zhí)行
    let i = 0, ret;
    do {
      ret = this.tasks[i++](...args);
    } while (!ret);
  }
}

在上面代碼的 call 方法中,我們?cè)O(shè)置返回值為 ret ,第一次執(zhí)行后沒(méi)有返回值則繼續(xù)循環(huán)執(zhí)行,如果有返回值則立即停止循環(huán),即實(shí)現(xiàn) “保險(xiǎn)” 的功能。

3、SyncWaterfallHook

SyncWaterfallHook 為串行同步執(zhí)行,上一個(gè)事件處理函數(shù)的返回值作為參數(shù)傳遞給下一個(gè)事件處理函數(shù),依次類推,正因如此,只有第一個(gè)事件處理函數(shù)的參數(shù)可以通過(guò) call 傳遞,而 call 的返回值為最后一個(gè)事件處理函數(shù)的返回值。

// SyncWaterfallHook 鉤子的使用
const { SyncWaterfallHook } = require("tapable");

// 創(chuàng)建實(shí)例
let syncWaterfallHook = new SyncWaterfallHook(["name", "age"]);

// 注冊(cè)事件
syncWaterfallHook.tap("1", (name, age) => {
  console.log("1", name, age);
  return "1";
});

syncWaterfallHook.tap("2", data => {
  console.log("2", data);
  return "2";
});

syncWaterfallHook.tap("3", data => {
  console.log("3", data);
  return "3"
});

// 觸發(fā)事件,讓監(jiān)聽(tīng)函數(shù)執(zhí)行
let ret = syncWaterfallHook.call("panda", 18);
console.log("call", ret);

// 1 panda 18
// 2 1
// 3 2
// call 3

SyncWaterfallHook 名稱中含有 “瀑布”,通過(guò)上面代碼可以看出 “瀑布” 形象生動(dòng)的描繪了事件處理函數(shù)執(zhí)行的特點(diǎn),與 SyncHook 和 SyncBailHook 的區(qū)別就在于事件處理函數(shù)返回結(jié)果的流動(dòng)性,接下來(lái)看一下 SyncWaterfallHook 類的實(shí)現(xiàn)。

// 模擬 SyncWaterfallHook 類
class SyncWaterfallHook {
  constructor(args) {
    this.args = args;
    this.tasks = [];
  }
  tap(name, task) {
    this.tasks.push(task);
  }
  call(...args) {
    // 傳入?yún)?shù)嚴(yán)格對(duì)應(yīng)創(chuàng)建實(shí)例傳入數(shù)組中的規(guī)定的參數(shù),執(zhí)行時(shí)多余的參數(shù)為 undefined
    args = args.slice(0, this.args.length);

    // 依次執(zhí)行事件處理函數(shù),事件處理函數(shù)的返回值作為下一個(gè)事件處理函數(shù)的參數(shù)
    let [first, ...others] = this.tasks;
    return reduce((ret, task) => task(ret), first(...args));
  }
}

上面代碼中 call 的邏輯是將存儲(chǔ)事件處理函數(shù)的 tasks 拆成兩部分,分別為第一個(gè)事件處理函數(shù),和存儲(chǔ)其余事件處理函數(shù)的數(shù)組,使用 reduce 進(jìn)行歸并,將第一個(gè)事件處理函數(shù)執(zhí)行后的返回值作為歸并的初始值,依次調(diào)用其余事件處理函數(shù)并傳遞上一次歸并的返回值。

4、SyncLoopHook

SyncLoopHook 為串行同步執(zhí)行,事件處理函數(shù)返回 true 表示繼續(xù)循環(huán),即循環(huán)執(zhí)行當(dāng)前事件處理函數(shù),返回 undefined 表示結(jié)束循環(huán), SyncLoopHook 與 SyncBailHook 的循環(huán)不同, SyncBailHook 只決定是否繼續(xù)向下執(zhí)行后面的事件處理函數(shù),而 SyncLoopHook 的循環(huán)是指循環(huán)執(zhí)行每一個(gè)事件處理函數(shù),直到返回 undefined 為止,才會(huì)繼續(xù)向下執(zhí)行其他事件處理函數(shù),執(zhí)行機(jī)制同理。

// SyncLoopHook 鉤子的使用
const { SyncLoopHook } = require("tapable");

// 創(chuàng)建實(shí)例
let syncLoopHook = new SyncLoopHook(["name", "age"]);

// 定義輔助變量
let total1 = 0;
let total2 = 0;

// 注冊(cè)事件
syncLoopHook.tap("1", (name, age) => {
  console.log("1", name, age, total1);
  return total1++ < 2 ? true : undefined;
});

syncLoopHook.tap("2", (name, age) => {
  console.log("2", name, age, total2);
  return total2++ < 2 ? true : undefined;
});

syncLoopHook.tap("3", (name, age) => console.log("3", name, age));

// 觸發(fā)事件,讓監(jiān)聽(tīng)函數(shù)執(zhí)行
syncLoopHook.call("panda", 18);

// 1 panda 18 0
// 1 panda 18 1
// 1 panda 18 2
// 2 panda 18 0
// 2 panda 18 1
// 2 panda 18 2
// 3 panda 18

通過(guò)上面的執(zhí)行結(jié)果可以清楚的看到 SyncLoopHook 的執(zhí)行機(jī)制,但有一點(diǎn)需要注意,返回值必須嚴(yán)格是 true 才會(huì)觸發(fā)循環(huán),多次執(zhí)行當(dāng)前事件處理函數(shù),必須嚴(yán)格返回 undefined ,才會(huì)結(jié)束循環(huán),去執(zhí)行后面的事件處理函數(shù),如果事件處理函數(shù)的返回值不是 true 也不是 undefined ,則會(huì)死循環(huán)。

在了解 SyncLoopHook 的執(zhí)行機(jī)制以后,我們接下來(lái)看看 SyncLoopHook 的 call 方法是如何實(shí)現(xiàn)的。

// 模擬 SyncLoopHook 類
class SyncLoopHook {
  constructor(args) {
    this.args = args;
    this.tasks = [];
  }
  tap(name, task) {
    this.tasks.push(task);
  }
  call(...args) {
    // 傳入?yún)?shù)嚴(yán)格對(duì)應(yīng)創(chuàng)建實(shí)例傳入數(shù)組中的規(guī)定的參數(shù),執(zhí)行時(shí)多余的參數(shù)為 undefined
    args = args.slice(0, this.args.length);

    // 依次執(zhí)行事件處理函數(shù),如果返回值為 true,則繼續(xù)執(zhí)行當(dāng)前事件處理函數(shù)
    // 直到返回 undefined,則繼續(xù)向下執(zhí)行其他事件處理函數(shù)
    this.tasks.forEach(task => {
      let ret;
      do {
        ret = this.task(...args);
      } while (ret === true || !(ret === undefined));
    });
  }
}

在上面代碼中可以看到 SyncLoopHook 類 call 方法的實(shí)現(xiàn)更像是 SyncHook 和 SyncBailHook 的 call 方法的結(jié)合版,外層循環(huán)整個(gè) tasks 事件處理函數(shù)隊(duì)列,內(nèi)層通過(guò)返回值進(jìn)行循環(huán),控制每一個(gè)事件處理函數(shù)的執(zhí)行次數(shù)。

注意:在 Sync 類型 “鉤子” 下執(zhí)行的插件都是順序執(zhí)行的,只能使用 tab 注冊(cè)。

Async 類型的鉤子

Async 類型可以使用 tap 、 tapSync 和 tapPromise 注冊(cè)不同類型的插件 “鉤子”,分別通過(guò) call 、 callAsync 和 promise 方法調(diào)用,我們下面會(huì)針對(duì) AsyncParallelHook 和 AsyncSeriesHook 的 async 和 promise 兩種方式分別介紹和模擬。

1、AsyncParallelHook

AsyncParallelHook 為異步并行執(zhí)行,通過(guò) tapAsync 注冊(cè)的事件,通過(guò) callAsync 觸發(fā),通過(guò) tapPromise 注冊(cè)的事件,通過(guò) promise 觸發(fā)(返回值可以調(diào)用 then 方法)。

(1) tapAsync/callAsync

callAsync 的最后一個(gè)參數(shù)為回調(diào)函數(shù),在所有事件處理函數(shù)執(zhí)行完畢后執(zhí)行。

// AsyncParallelHook 鉤子:tapAsync/callAsync 的使用
const { AsyncParallelHook } = require("tapable");

// 創(chuàng)建實(shí)例
let asyncParallelHook = new AsyncParallelHook(["name", "age"]);

// 注冊(cè)事件
console.time("time");
asyncParallelHook.tapAsync("1", (name, age, done) => {
  settimeout(() => {
    console.log("1", name, age, new Date());
    done();
  }, 1000);
});

asyncParallelHook.tapAsync("2", (name, age, done) => {
  settimeout(() => {
    console.log("2", name, age, new Date());
    done();
  }, 2000);
});

asyncParallelHook.tapAsync("3", (name, age, done) => {
  settimeout(() => {
    console.log("3", name, age, new Date());
    done();
    console.timeEnd("time");
  }, 3000);
});

// 觸發(fā)事件,讓監(jiān)聽(tīng)函數(shù)執(zhí)行
asyncParallelHook.callAsync("panda", 18, () => {
  console.log("complete");
});

// 1 panda 18 2018-08-07T10:38:32.675Z
// 2 panda 18 2018-08-07T10:38:33.674Z
// 3 panda 18 2018-08-07T10:38:34.674Z
// complete
// time: 3005.060ms

異步并行是指,事件處理函數(shù)內(nèi)三個(gè)定時(shí)器的異步操作最長(zhǎng)時(shí)間為 3s ,而三個(gè)事件處理函數(shù)執(zhí)行完成總共用時(shí)接近 3s ,所以三個(gè)事件處理函數(shù)是幾乎同時(shí)執(zhí)行的,不需等待。

所有 tabAsync 注冊(cè)的事件處理函數(shù)最后一個(gè)參數(shù)都為一個(gè)回調(diào)函數(shù) done ,每個(gè)事件處理函數(shù)在異步代碼執(zhí)行完畢后調(diào)用 done 函數(shù),則可以保證 callAsync 會(huì)在所有異步函數(shù)都執(zhí)行完畢后執(zhí)行,接下來(lái)看一看 callAsync 是如何實(shí)現(xiàn)的。

// 模擬 AsyncParallelHook 類:tapAsync/callAsync
class AsyncParallelHook {
  constructor(args) {
    this.args = args;
    this.tasks = [];
  }
  tabAsync(name, task) {
    this.tasks.push(task);
  }
  callAsync(...args) {
    // 先取出最后傳入的回調(diào)函數(shù)
    let finalCallback = args.pop();

    // 傳入?yún)?shù)嚴(yán)格對(duì)應(yīng)創(chuàng)建實(shí)例傳入數(shù)組中的規(guī)定的參數(shù),執(zhí)行時(shí)多余的參數(shù)為 undefined
    args = args.slice(0, this.args.length);

    // 定義一個(gè) i 變量和 done 函數(shù),每次執(zhí)行檢測(cè) i 值和隊(duì)列長(zhǎng)度,決定是否執(zhí)行 callAsync 的回調(diào)函數(shù)
    let i = 0;
    let done = () => {
      if (++i === this.tasks.length) {
        finalCallback();
      }
    };

    // 依次執(zhí)行事件處理函數(shù)
    this.tasks.forEach(task => task(...args, done));
  }
}

在 callAsync 中,將最后一個(gè)參數(shù)(所有事件處理函數(shù)執(zhí)行完畢后執(zhí)行的回調(diào))取出,并定義 done 函數(shù),通過(guò)比較 i 和存儲(chǔ)事件處理函數(shù)的數(shù)組 tasks 的 length 來(lái)確定回調(diào)是否執(zhí)行,循環(huán)執(zhí)行每一個(gè)事件處理函數(shù)并將 done 作為最后一個(gè)參數(shù)傳入,所以每個(gè)事件處理函數(shù)內(nèi)部的異步操作完成時(shí),執(zhí)行 done 就是為了檢測(cè)是不是該執(zhí)行 callAsync 的回調(diào),當(dāng)所有事件處理函數(shù)均執(zhí)行完畢滿足 done 函數(shù)內(nèi)部 i 和 length 相等的條件時(shí),則調(diào)用 callAsync 的回調(diào)。

(2) tapPromise/promise

要使用 tapPromise 注冊(cè)事件,對(duì)事件處理函數(shù)有一個(gè)要求,必須返回一個(gè) Promise 實(shí)例,而 promise 方法也返回一個(gè) Promise 實(shí)例, callAsync 的回調(diào)函數(shù)在 promise 方法中用 then 的方式代替。

// AsyncParallelHook 鉤子:tapPromise/promise 的使用
const { AsyncParallelHook } = require("tapable");

// 創(chuàng)建實(shí)例
let asyncParallelHook = new AsyncParallelHook(["name", "age"]);

// 注冊(cè)事件
console.time("time");
asyncParallelHook.tapPromise("1", (name, age) => {
  return new Promise((resolve, reject) => {
    settimeout(() => {
      console.log("1", name, age, new Date());
      resolve("1");
    }, 1000);
  });
});

asyncParallelHook.tapPromise("2", (name, age) => {
  return new Promise((resolve, reject) => {
    settimeout(() => {
      console.log("2", name, age, new Date());
      resolve("2");
    }, 2000);
  });
});

asyncParallelHook.tapPromise("3", (name, age) => {
  return new Promise((resolve, reject) => {
    settimeout(() => {
      console.log("3", name, age, new Date());
      resolve("3");
      console.timeEnd("time");
    }, 3000);
  });
});

// 觸發(fā)事件,讓監(jiān)聽(tīng)函數(shù)執(zhí)行
asyncParallelHook.promise("panda", 18).then(ret => {
  console.log(ret);
});

// 1 panda 18 2018-08-07T12:17:21.741Z
// 2 panda 18 2018-08-07T12:17:22.736Z
// 3 panda 18 2018-08-07T12:17:23.739Z
// time: 3006.542ms
// [ '1', '2', '3' ]

上面每一個(gè) tapPromise 注冊(cè)事件的事件處理函數(shù)都返回一個(gè) Promise 實(shí)例,并將返回值傳入 resolve 方法,調(diào)用 promise 方法觸發(fā)事件時(shí),如果所有事件處理函數(shù)返回的 Promise 實(shí)例結(jié)果都成功,會(huì)將結(jié)果存儲(chǔ)在數(shù)組中,并作為參數(shù)傳遞給 promise 的 then 方法中成功的回調(diào),如果有一個(gè)失敗就是將失敗的結(jié)果返回作為參數(shù)傳遞給失敗的回調(diào)。

// 模擬 AsyncParallelHook 類 tapPromise/promise
class AsyncParallelHook {
  constructor(args) {
    this.args = args;
    this.tasks = [];
  }
  tapPromise(name, task) {
    this.tasks.push(task);
  }
  promise(...args) {
    // 傳入?yún)?shù)嚴(yán)格對(duì)應(yīng)創(chuàng)建實(shí)例傳入數(shù)組中的規(guī)定的參數(shù),執(zhí)行時(shí)多余的參數(shù)為 undefined
    args = args.slice(0, this.args.length);

    // 將所有事件處理函數(shù)轉(zhuǎn)換成 Promise 實(shí)例,并發(fā)執(zhí)行所有的 Promise
    return Promise.all(this.tasks.map(task => task(...args)));
  }
}

其實(shí)根據(jù)上面對(duì)于 tapPromise 和 promise 使用的描述就可以猜到, promise 方法的邏輯是通過(guò) Promise.all 來(lái)實(shí)現(xiàn)的。

2、AsyncSeriesHook

AsyncSeriesHook 為異步串行執(zhí)行,與 AsyncParallelHook 相同,通過(guò) tapAsync 注冊(cè)的事件,通過(guò) callAsync 觸發(fā),通過(guò) tapPromise 注冊(cè)的事件,通過(guò) promise 觸發(fā),可以調(diào)用 then 方法。

(1) tapAsync/callAsync

與 AsyncParallelHook 的 callAsync 方法類似, AsyncSeriesHook 的 callAsync 方法也是通過(guò)傳入回調(diào)函數(shù)的方式,在所有事件處理函數(shù)執(zhí)行完畢后執(zhí)行 callAsync 的回調(diào)函數(shù)。

// AsyncSeriesHook 鉤子:tapAsync/callAsync 的使用
const { AsyncSeriesHook } = require("tapable");

// 創(chuàng)建實(shí)例
let asyncSeriesHook = new AsyncSeriesHook(["name", "age"]);

// 注冊(cè)事件
console.time("time");
asyncSeriesHook.tapAsync("1", (name, age, next) => {
  settimeout(() => {
    console.log("1", name, age, new Date());
    next();
  }, 1000);
});

asyncSeriesHook.tapAsync("2", (name, age, next) => {
  settimeout(() => {
    console.log("2", name, age, new Date());
    next();
  }, 2000);
});

asyncSeriesHook.tapAsync("3", (name, age, next) => {
  settimeout(() => {
    console.log("3", name, age, new Date());
    next();
    console.timeEnd("time");
  }, 3000);
});

// 觸發(fā)事件,讓監(jiān)聽(tīng)函數(shù)執(zhí)行
asyncSeriesHook.callAsync("panda", 18, () => {
  console.log("complete");
});

// 1 panda 18 2018-08-07T14:40:52.896Z
// 2 panda 18 2018-08-07T14:40:54.901Z
// 3 panda 18 2018-08-07T14:40:57.901Z
// complete
// time: 6008.790ms

異步串行是指,事件處理函數(shù)內(nèi)三個(gè)定時(shí)器的異步執(zhí)行時(shí)間分別為 1s 、 2s 和 3s ,而三個(gè)事件處理函數(shù)執(zhí)行完總共用時(shí)接近 6s ,所以三個(gè)事件處理函數(shù)執(zhí)行是需要排隊(duì)的,必須一個(gè)一個(gè)執(zhí)行,當(dāng)前事件處理函數(shù)執(zhí)行完才能執(zhí)行下一個(gè)。

AsyncSeriesHook 類的 tabAsync 方法注冊(cè)的事件處理函數(shù)參數(shù)中的 next 可以與 AsyncParallelHook 類中 tabAsync 方法參數(shù)的 done 進(jìn)行類比,同為回調(diào)函數(shù),不同點(diǎn)在于 AsyncSeriesHook 與 AsyncParallelHook 的 callAsync 方法的 “并行” 和 “串行” 的實(shí)現(xiàn)方式。

// 模擬 AsyncSeriesHook 類:tapAsync/callAsync
class AsyncSeriesHook {
  constructor(args) {
    this.args = args;
    this.tasks = [];
  }
  tabAsync(name, task) {
    this.tasks.push(task);
  }
  callAsync(...args) {
    // 先取出最后傳入的回調(diào)函數(shù)
    let finalCallback = args.pop();

    // 傳入?yún)?shù)嚴(yán)格對(duì)應(yīng)創(chuàng)建實(shí)例傳入數(shù)組中的規(guī)定的參數(shù),執(zhí)行時(shí)多余的參數(shù)為 undefined
    args = args.slice(0, this.args.length);

    // 定義一個(gè) i 變量和 next 函數(shù),每次取出一個(gè)事件處理函數(shù)執(zhí)行,并維護(hù) i 的值
    // 直到所有事件處理函數(shù)都執(zhí)行完,調(diào)用 callAsync 的回調(diào)
    // 如果事件處理函數(shù)中沒(méi)有調(diào)用 next,則無(wú)法繼續(xù)
    let i = 0;
    let next = () => {
      let task = this.tasks[i++];
      task ? task(...args, next) : finalCallback();
    };
    next();
  }
}

AsyncParallelHook 是通過(guò)循環(huán)依次執(zhí)行了所有的事件處理函數(shù), done 方法只為了檢測(cè)是否已經(jīng)滿足條件執(zhí)行 callAsync 的回調(diào),如果中間某個(gè)事件處理函數(shù)沒(méi)有調(diào)用 done ,只是不會(huì)調(diào)用 callAsync 的回調(diào),但是所有的事件處理函數(shù)都執(zhí)行了。

而 AsyncSeriesHook 的 next 執(zhí)行機(jī)制更像 Express 和 Koa 中的中間件,在注冊(cè)事件的回調(diào)中如果不調(diào)用 next ,則在觸發(fā)事件時(shí)會(huì)在沒(méi)有調(diào)用 next 的事件處理函數(shù)的位置 “卡死”,即不會(huì)繼續(xù)執(zhí)行后面的事件處理函數(shù),只有都調(diào)用 next 才能繼續(xù),而最后一個(gè)事件處理函數(shù)中調(diào)用 next 決定是否調(diào)用 callAsync 的回調(diào)。

(2) tapPromise/promise

與 AsyncParallelHook 類似, tapPromise 注冊(cè)事件的事件處理函數(shù)需要返回一個(gè) Promise 實(shí)例, promise 方法最后也返回一個(gè) Promise 實(shí)例。

// AsyncSeriesHook 鉤子:tapPromise/promise 的使用
const { AsyncSeriesHook } = require("tapable");

// 創(chuàng)建實(shí)例
let asyncSeriesHook = new AsyncSeriesHook(["name", "age"]);

// 注冊(cè)事件
console.time("time");
asyncSeriesHook.tapPromise("1", (name, age) => {
  return new Promise((resolve, reject) => {
    settimeout(() => {
      console.log("1", name, age, new Date());
      resolve("1");
    }, 1000);
  });
});

asyncSeriesHook.tapPromise("2", (name, age) => {
  return new Promise((resolve, reject) => {
    settimeout(() => {
      console.log("2", name, age, new Date());
      resolve("2");
    }, 2000);
  });
});

asyncParallelHook.tapPromise("3", (name, age) => {
  return new Promise((resolve, reject) => {
    settimeout(() => {
      console.log("3", name, age, new Date());
      resolve("3");
      console.timeEnd("time");
    }, 3000);
  });
});

// 觸發(fā)事件,讓監(jiān)聽(tīng)函數(shù)執(zhí)行
asyncSeriesHook.promise("panda", 18).then(ret => {
  console.log(ret);
});

// 1 panda 18 2018-08-07T14:45:52.896Z
// 2 panda 18 2018-08-07T14:45:54.901Z
// 3 panda 18 2018-08-07T14:45:57.901Z
// time: 6014.291ms
// [ '1', '2', '3' ]

分析上面的執(zhí)行過(guò)程,所有的事件處理函數(shù)都返回了 Promise 的實(shí)例,如果想實(shí)現(xiàn) “串行”,則需要讓每一個(gè)返回的 Promise 實(shí)例都調(diào)用 then ,并在 then 中執(zhí)行下一個(gè)事件處理函數(shù),這樣就保證了只有上一個(gè)事件處理函數(shù)執(zhí)行完后才會(huì)執(zhí)行下一個(gè)。

// 模擬 AsyncSeriesHook 類 tapPromise/promise
class AsyncSeriesHook {
  constructor(args) {
    this.args = args;
    this.tasks = [];
  }
  tapPromise(name, task) {
    this.tasks.push(task);
  }
  promise(...args) {
    // 傳入?yún)?shù)嚴(yán)格對(duì)應(yīng)創(chuàng)建實(shí)例傳入數(shù)組中的規(guī)定的參數(shù),執(zhí)行時(shí)多余的參數(shù)為 undefined
    args = args.slice(0, this.args.length);

    // 將每個(gè)事件處理函數(shù)執(zhí)行并調(diào)用返回 Promise 實(shí)例的 then 方法
    // 讓下一個(gè)事件處理函數(shù)在 then 方法成功的回調(diào)中執(zhí)行
    let [first, ...others] = this.tasks;
    return others.reduce((promise, task) => {
      return promise.then(() => task(...args));
    }, first(...args));
  }
}

上面代碼中的 “串行” 是使用 reduce 歸并來(lái)實(shí)現(xiàn)的,首先將存儲(chǔ)所有事件處理函數(shù)的數(shù)組 tasks 解構(gòu)成兩部分,第一個(gè)事件處理函數(shù)和存儲(chǔ)其他事件處理函數(shù)的數(shù)組 others ,對(duì) others 進(jìn)行歸并,將第一個(gè)事件處理函數(shù)執(zhí)行后返回的 Promise 實(shí)例作為歸并的初始值,這樣在歸并的過(guò)程中上一個(gè)值始終是上一個(gè)事件處理函數(shù)返回的 Promise 實(shí)例,可以直接調(diào)用 then 方法,并在 then 的回調(diào)中執(zhí)行下一個(gè)事件處理函數(shù),直到歸并完成,將 reduce 最后返回的 Promise 實(shí)例作為 promise 方法的返回值,則實(shí)現(xiàn) promise 方法執(zhí)行后繼續(xù)調(diào)用 then 來(lái)實(shí)現(xiàn)后續(xù)邏輯。

對(duì)其他異步鉤子補(bǔ)充

在上面 Async 異步類型的 “鉤子中”,我們只著重介紹了 “串行” 和 “并行”( AsyncParallelHook 和 AsyncSeriesHook )以及回調(diào)和 Promise 的兩種注冊(cè)和觸發(fā)事件的方式,還有一些其他的具有一定特點(diǎn)的異步 “鉤子” 我們并沒(méi)有進(jìn)行分析,因?yàn)樗麄兊臋C(jī)制與同步對(duì)應(yīng)的 “鉤子” 非常的相似。

AsyncParallelBailHook 和 AsyncSeriesBailHook 分別為異步 “并行” 和 “串行” 執(zhí)行的 “鉤子”,返回值不為 undefined ,即有返回值,則立即停止向下執(zhí)行其他事件處理函數(shù),實(shí)現(xiàn)邏輯可結(jié)合 AsyncParallelHook 、 AsyncSeriesHook 和 SyncBailHook 。

AsyncSeriesWaterfallHook 為異步 “串行” 執(zhí)行的 “鉤子”,上一個(gè)事件處理函數(shù)的返回值作為參數(shù)傳遞給下一個(gè)事件處理函數(shù),實(shí)現(xiàn)邏輯可結(jié)合 AsyncSeriesHook 和 SyncWaterfallHook 。

總結(jié)

在 tapable 源碼中,注冊(cè)事件的方法 tab 、 tapSync 、 tapPromise 和觸發(fā)事件的方法 call 、 callAsync 、 promise 都是通過(guò) compile 方法快速編譯出來(lái)的,我們本文中這些方法的實(shí)現(xiàn)只是遵照了 tapable 庫(kù)這些 “鉤子” 的事件處理機(jī)制進(jìn)行了模擬,以方便我們了解 tapable ,為學(xué)習(xí) Webpack 原理做了一個(gè)鋪墊,在 Webpack 中,這些 “鉤子” 的真正作用就是將通過(guò)配置文件讀取的插件與插件、加載器與加載器之間進(jìn)行連接,“并行” 或 “串行” 執(zhí)行,相信在我們對(duì) tapable 中這些 “鉤子” 的事件機(jī)制有所了解之后,再重新學(xué)習(xí) Webpack 的源碼應(yīng)該會(huì)有所頭緒。

以上就是本文的全部?jī)?nèi)容,希望對(duì)大家的學(xué)習(xí)有所幫助,也希望大家多多支持億速云。

向AI問(wèn)一下細(xì)節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如果涉及侵權(quán)請(qǐng)聯(lián)系站長(zhǎng)郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI