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方法里的处理逻辑是将上一个事件执行返回的结果做为下一个事件的第一个参数传了进去而已。有兴趣的朋友能够按照本文所述的顺序去阅读下源码。

相关文章
相关标签/搜索