在解析webpack4 的 Compiler 模塊前,咱們先要解析如下它賴以實現的也是webpack的核心依賴模塊tapable。javascript
tapable 簡而言之,就是一個註冊鉤子函數的模塊。 php
咱們知道,webpack之因此強大,靠的就是豐富的插件系統,無論你有什麼需求,總有插件能知足你。而這些插件可以按照你配置的方式工做,所有依賴於tapable模塊,它將這些插件註冊爲一個個鉤子函數,而後按照插件註冊時告知的方式,在合適的時機安排它們運行,最終完成整個打包任務。java
tapable 的基本工做流程以下:node
下面咱們分別來講。webpack
在webpack的Compiler.js中,咱們能夠看到以下的引入代碼:web
…… const { Tapable, SyncHook, SyncBailHook, AsyncParallelHook, AsyncSeriesHook } = require("tapable"); ……
咱們看到,除了引入Tapable自己,它引入了四種鉤子,其中以Sync開頭的爲同步類型的鉤子,而以Async開頭的則爲異步類型的鉤子。數組
這意味着,以同步類型的鉤子註冊的事件,將以同步的方式執行,而以異步類型的鉤子註冊的事件則以異步的方式執行。promise
其實在tapable中,不止上面四種類型的鉤子,打開tapable源碼,咱們能夠看到:異步
其中,以藍色線條框住的就是異步類型鉤子,以橘紅色線條框住的爲同步類型的鉤子,下面分別說明下它們的執行機制。async
true
表示繼續循環,即循環執行當前事件處理函數,返回 undefined
表示結束循環tapAsync
註冊的事件,經過 callAsync
觸發,經過 tapPromise
註冊的事件,經過 promise
觸發(返回值能夠調用 then
方法)AsyncParallelHook
相同,經過 tapAsync
註冊的事件,經過 callAsync
觸發,經過 tapPromise
註冊的事件,經過 promise
觸發,能夠調用 then
方法。AsyncParallelHook
相同可是若是其中一個事件有返回值,則當即中止執行。AsyncSeriesHook
相同,可是若是其中一個事件有返回值,則當即中止執行。undefined
而中止。在Complier.js中,咱們能夠看到一開始在Complier類中就實例化了不少鉤子實例:
this.hooks = { /** @type {SyncBailHook<Compilation>} */ shouldEmit: new SyncBailHook(["compilation"]), /** @type {AsyncSeriesHook<Stats>} */ done: new AsyncSeriesHook(["stats"]), /** @type {AsyncSeriesHook<>} */ additionalPass: new AsyncSeriesHook([]), /** @type {AsyncSeriesHook<Compiler>} */ beforeRun: new AsyncSeriesHook(["compiler"]), /** @type {AsyncSeriesHook<Compiler>} */ run: new AsyncSeriesHook(["compiler"]), /** @type {AsyncSeriesHook<Compilation>} */ emit: new AsyncSeriesHook(["compilation"]), /** @type {AsyncSeriesHook<Compilation>} */ afterEmit: new AsyncSeriesHook(["compilation"]), /** @type {SyncHook<Compilation, CompilationParams>} */ thisCompilation: new SyncHook(["compilation", "params"]), /** @type {SyncHook<Compilation, CompilationParams>} */ compilation: new SyncHook(["compilation", "params"]), /** @type {SyncHook<NormalModuleFactory>} */ normalModuleFactory: new SyncHook(["normalModuleFactory"]), /** @type {SyncHook<ContextModuleFactory>} */ contextModuleFactory: new SyncHook(["contextModulefactory"]), /** @type {AsyncSeriesHook<CompilationParams>} */ beforeCompile: new AsyncSeriesHook(["params"]), /** @type {SyncHook<CompilationParams>} */ compile: new SyncHook(["params"]), /** @type {AsyncParallelHook<Compilation>} */ make: new AsyncParallelHook(["compilation"]), /** @type {AsyncSeriesHook<Compilation>} */ afterCompile: new AsyncSeriesHook(["compilation"]), /** @type {AsyncSeriesHook<Compiler>} */ watchRun: new AsyncSeriesHook(["compiler"]), /** @type {SyncHook<Error>} */ failed: new SyncHook(["error"]), /** @type {SyncHook<string, string>} */ invalid: new SyncHook(["filename", "changeTime"]), /** @type {SyncHook} */ watchClose: new SyncHook([]), // TODO the following hooks are weirdly located here // TODO move them for webpack 5 /** @type {SyncHook} */ environment: new SyncHook([]), /** @type {SyncHook} */ afterEnvironment: new SyncHook([]), /** @type {SyncHook<Compiler>} */ afterPlugins: new SyncHook(["compiler"]), /** @type {SyncHook<Compiler>} */ afterResolvers: new SyncHook(["compiler"]), /** @type {SyncBailHook<string, Entry>} */ entryOption: new SyncBailHook(["context", "entry"]) };
註冊事件通常同步類型的鉤子使用tap方法註冊,而異步類型的鉤子通常使用tapAsync方法類註冊。
好比,在webpack包內的 APIPlugin.js中,就是這樣註冊的:
而在CachePlugin.js中,則是這樣註冊的:
在上面的鉤子實例化時,咱們能夠看到 compilation 鉤子是一個同步類型的鉤子,而run 則是一個異步類型的鉤子。
咱們以上面 shouldEmit
爲例來看,它是在Complier.js的第230觸發了事件的:
if (this.hooks.shouldEmit.call(compilation) === false) { const stats = new Stats(compilation); stats.startTime = startTime; stats.endTime = Date.now(); this.hooks.done.callAsync(stats, err => { if (err) return finalCallback(err); return finalCallback(null, stats); }); return; }
咱們能夠從上面實例化的代碼中看到,shouldEmit
是一個同步類型的鉤子,在這裏觸發事件時,它使用call
方法來傳遞參數,咱們看到這裏的參數是一個布爾值。而上面代碼的第5行,down
是一個異步類型的鉤子,它則使用callAsycn
方法來註冊事件,它則傳入了一個stats
對象和一個錯誤處理函數。
其實,觸發事件一共有下面幾種方式:
而根據鉤子類型的不一樣,異步類型的鉤子還能夠在後面加上Asycn
實際上,tapable 本質上是一個相似於nodejs 的 events 模塊的事件發佈器。咱們看一下如下代碼:
const EventEmitter = require('events'); const myEmitter = new EventEmitter(); /** * param1 事件名 * param2 回調函數 */ myEmitter.on('run',(arg1,arg2)=>{ console.log("run",arg1,arg2); }); // 在這裏發佈事件 myEmitter.emit('run',111,222); // run 111 222
能夠看到,事件發佈器是使用on來註冊一個事件的監聽,而使用emit來發布(觸發)這個事件。tapable本質上作的工做和它是同樣的,不過是使用tap等方法來註冊事件,用call等方法來發布事件而已。
經過閱讀tapable 咱們能夠發現,全部的鉤子都繼承自 Hook 類,那咱們先看下Hook類的構造函數:
constructor(args) { if (!Array.isArray(args)) args = []; this._args = args; this.taps = []; this.interceptors = []; this.call = this._call; this.promise = this._promise; this.callAsync = this._callAsync; this._x = undefined; }
咱們能夠看到,每個鉤子都擁有一個taps數組,一個攔截器數組(interceptors),還有三個調用方法,分別對應普通同步調用(call),異步調用(callAsync)和承諾調用(promise)。
而三個事件註冊方法也在類的定義中初現:
tap(options, fn) { if (typeof options === "string") options = { name: options }; if (typeof options !== "object" || options === null) throw new Error( "Invalid arguments to tap(options: Object, fn: function)" ); options = Object.assign({ type: "sync", fn: fn }, options); if (typeof options.name !== "string" || options.name === "") throw new Error("Missing name for tap"); options = this._runRegisterInterceptors(options); this._insert(options); } tapAsync(options, fn) { …… } tapPromise(options, fn) { …… }
這三個方法,除了在合併對象時傳入的 type 值不一樣,其它都相同。註冊的實質就是將傳入的選項和方法都合併到一個總的options對象裏,而後使用_insert內部方法將這個對象扔進了 taps 數組中。中間還檢查了是否認義了攔截器,若是有攔截器註冊方法,則將當前事件註冊到攔截器數組中。
在Hook類中,咱們還應該注意,三個事件調用方法是經過 createCompileDelegate 方法調用_createCall 方法來生成,而且經過defineProperties方法定義到了Hook類的原型上面。
//這個方法返回了一個編譯後的鉤子實例 _createCall(type) { return this.compile({ taps: this.taps, interceptors: this.interceptors, args: this._args, type: type }); } …… // 建立編譯的代理方法,返回了一個調用時才執行的鉤子生成方法 function createCompileDelegate(name, type) { return function lazyCompileHook(...args) { this[name] = this._createCall(type); return this[name](...args); }; } //將調用方法定義到了原型上 Object.defineProperties(Hook.prototype, { _call: { value: createCompileDelegate("call", "sync"), configurable: true, writable: true }, _promise: { value: createCompileDelegate("promise", "promise"), configurable: true, writable: true }, _callAsync: { value: createCompileDelegate("callAsync", "async"), configurable: true, writable: true } });
在上層,全部的鉤子都是由鉤子工廠生成,而全部類型的鉤子工廠都繼承自鉤子工廠類:
class HookCodeFactory { constructor(config) { this.config = config; this.options = undefined; this._args = undefined; } create(options) { …… } setup(instance, options) { instance._x = options.taps.map(t => t.fn); } /** * @param {{ type: "sync" | "promise" | "async", taps: Array<Tap>, interceptors: Array<Interceptor> }} options */ init(options) { this.options = options; this._args = options.args.slice(); } deinit() { this.options = undefined; this._args = undefined; } header() { …… } needContext() { for (const tap of this.options.taps) if (tap.context) return true; return false; } callTap(tapIndex, { onError, onResult, onDone, rethrowIfPossible }) { …… } callTapsSeries({}) { …… } callTapsLooping({ onError, onDone, rethrowIfPossible }) { …… } callTapsParallel({ onError, onResult, onDone, rethrowIfPossible, onTap = (i, run) => run() }) { …… } args({ before, after } = {}) { …… } getTapFn(idx) { return `_x[${idx}]`; } getTap(idx) { return `_taps[${idx}]`; } getInterceptor(idx) { return `_interceptors[${idx}]`; } }
咱們發現,在鉤子工廠中,完成了對鉤子的建立、初始化和配置等工做,而且實現了各類類型的基本調用方法的代碼生成方法。
有了基本的鉤子類和鉤子工廠類,就能夠用它們來生成各類同步/異步、串行/並行、熔斷/流水類型的鉤子了,咱們以SyncBailHook爲例來看:
/* MIT License http://www.opensource.org/licenses/mit-license.php Author Tobias Koppers @sokra */ "use strict"; const Hook = require("./Hook"); const HookCodeFactory = require("./HookCodeFactory"); class SyncBailHookCodeFactory extends HookCodeFactory { content({ onError, onResult, resultReturns, onDone, rethrowIfPossible }) { return this.callTapsSeries({ onError: (i, err) => onError(err), onResult: (i, result, next) => `if(${result} !== undefined) {\n${onResult( result )};\n} else {\n${next()}}\n`, resultReturns, onDone, rethrowIfPossible }); } } const factory = new SyncBailHookCodeFactory(); class SyncBailHook extends Hook { tapAsync() { throw new Error("tapAsync is not supported on a SyncBailHook"); } tapPromise() { throw new Error("tapPromise is not supported on a SyncBailHook"); } compile(options) { factory.setup(this, options); return factory.create(options); } } module.exports = SyncBailHook;
能夠看到,它先是繼承了基礎的鉤子工廠,並經過調用 callTapsSeries 方法返回了一個串行的鉤子實例,而且在onResult方法裏,加了一個if判斷,若是結果不爲空,就中止,不然執行下一個事件,這就是熔斷機制。
而後下面實例化了一個該類型的工廠,利用這個工廠配置了對鉤子實例進行了配置(setup)和生成(create)。
其它類型的鉤子類的實現也大同小異。只不過並行類的鉤子再也不調用callTapsSeries 方法,而是調用callTapsParallel 方法,而像 Waterfall 型的鉤子則在onResult方法裏的處理邏輯是將上一個事件執行返回的結果做爲下一個事件的第一個參數傳了進去而已。有興趣的朋友能夠按照本文所述的順序去閱讀下源碼。