webpack4 源碼解析(三)——tapable

webpack4 源碼解析(三)——tapable

在解析webpack4 的 Compiler 模塊前,咱們先要解析如下它賴以實現的也是webpack的核心依賴模塊tapable。javascript

tapable 簡而言之,就是一個註冊鉤子函數的模塊。 php

咱們知道,webpack之因此強大,靠的就是豐富的插件系統,無論你有什麼需求,總有插件能知足你。而這些插件可以按照你配置的方式工做,所有依賴於tapable模塊,它將這些插件註冊爲一個個鉤子函數,而後按照插件註冊時告知的方式,在合適的時機安排它們運行,最終完成整個打包任務。java

工做流程

tapable 的基本工做流程以下:node

  1. 引入鉤子類型
  2. 建立該鉤子類型的實例
  3. 註冊事件
  4. 觸發事件,讓監聽函數執行

下面咱們分別來講。webpack

引入鉤子類型

在webpack的Compiler.js中,咱們能夠看到以下的引入代碼:web

……
const {
    Tapable,
    SyncHook,
    SyncBailHook,
    AsyncParallelHook,
    AsyncSeriesHook
} = require("tapable");
……

咱們看到,除了引入Tapable自己,它引入了四種鉤子,其中以Sync開頭的爲同步類型的鉤子,而以Async開頭的則爲異步類型的鉤子。數組

這意味着,以同步類型的鉤子註冊的事件,將以同步的方式執行,而以異步類型的鉤子註冊的事件則以異步的方式執行。promise

其實在tapable中,不止上面四種類型的鉤子,打開tapable源碼,咱們能夠看到:異步

其中,以藍色線條框住的就是異步類型鉤子,以橘紅色線條框住的爲同步類型的鉤子,下面分別說明下它們的執行機制。async

同步鉤子

  • SyncHook: 串行同步執行,不關心事件處理函數的返回值,在觸發事件以後,會按照事件註冊的前後順序執行全部的事件處理函數。
  • SyncBailHook: 串行同步執行,若是事件處理函數執行時有一個返回值不爲空(即有返回值),則跳過剩下未執行的事件處理函數
  • SyncWaterfallHook: 爲串行同步執行,上一個事件處理函數的返回值做爲參數傳遞給下一個事件處理函數,依次類推
  • SyncLoopHook:串行同步執行,事件處理函數返回 true 表示繼續循環,即循環執行當前事件處理函數,返回 undefined 表示結束循環

異步鉤子

  • AsycnParallelHook:異步並行執行,經過 tapAsync 註冊的事件,經過 callAsync 觸發,經過 tapPromise 註冊的事件,經過 promise 觸發(返回值能夠調用 then 方法)
  • AsyncSeriesHook:異步串行執行,與 AsyncParallelHook 相同,經過 tapAsync 註冊的事件,經過 callAsync 觸發,經過 tapPromise 註冊的事件,經過 promise 觸發,能夠調用 then 方法。
  • AsncParallelBailHook: 異步並行執行,與 AsyncParallelHook 相同可是若是其中一個事件有返回值,則當即中止執行。
  • AsyncSeriesBailHook:異步串行執行,與 AsyncSeriesHook 相同,可是若是其中一個事件有返回值,則當即中止執行。
  • AsyncSeriesLoopHook:異步串行執行,循環執行全部註冊事件直到某個事件返回undefined 而中止。
  • AsyncSeriesWaterfallHook:異步串行執行,上一個事件處理函數的返回值做爲參數傳遞給下一個事件處理函數,一次類推。

建立實例

在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對象和一個錯誤處理函數。

其實,觸發事件一共有下面幾種方式:

  • call: 鉤子觸發時調用
  • loop:觸發循環類鉤子的每一個循環事件
  • register:觸發每個添加的Tab對象,而且容許修改Tab對象

而根據鉤子類型的不一樣,異步類型的鉤子還能夠在後面加上Asycn

工做原理

nodejs 的 events模塊

實際上,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方法裏的處理邏輯是將上一個事件執行返回的結果做爲下一個事件的第一個參數傳了進去而已。有興趣的朋友能夠按照本文所述的順序去閱讀下源碼。

相關文章
相關標籤/搜索