在查看webpack(及插件)源码的过程中,我们会经常看到类似这样的代码

compiler.hooks.done.tap('vue-cli-service serve',() => {});
// 或者
compilation.hooks.buildModule.tap("ProgressPlugin", moduleAdd);

或者

compiler.hooks.environment.call();

即调用hooks.xxx.tap(还有类似的tapPromise/tapAsync)方法hooks.xxx.call(还有类似的promise/callAsync)方法。首先一般是Compiler和Compilation类的实例里面会调用上市tap和call方法。首先,它们都继承自Tapable类,并在其构造函数里面都有一个hooks的对象,该对象的value都是一个Hook的子类的实例。以Compiler类为例。

class Compiler extends Tapable {
    constructor(context) {
        super();
        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"]),
            // ...
        }
    }
}

先看看基类Hook

// node_module/tapable/Hook.js
// node_module/tapable/Hook.js
class 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;
    }

    compile(options) {
        throw new Error("Abstract: should be overriden");
    }

    _createCall(type) {
      // 调用对应Hook的compile方法了
        return this.compile({
            taps: this.taps,
            interceptors: this.interceptors,
            args: this._args,
            type: type
        });
    }

    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) {
        if (typeof options === "string") options = { name: options };
        if (typeof options !== "object" || options === null)
            throw new Error(
                "Invalid arguments to tapAsync(options: Object, fn: function)"
            );
        options = Object.assign({ type: "async", fn: fn }, options);
        if (typeof options.name !== "string" || options.name === "")
            throw new Error("Missing name for tapAsync");
        options = this._runRegisterInterceptors(options);
        this._insert(options);
    }

    tapPromise(options, fn) {
        if (typeof options === "string") options = { name: options };
        if (typeof options !== "object" || options === null)
            throw new Error(
                "Invalid arguments to tapPromise(options: Object, fn: function)"
            );
        options = Object.assign({ type: "promise", fn: fn }, options);
        if (typeof options.name !== "string" || options.name === "")
            throw new Error("Missing name for tapPromise");
        options = this._runRegisterInterceptors(options);
        this._insert(options);
    }

    _runRegisterInterceptors(options) {
        for (const interceptor of this.interceptors) {
            if (interceptor.register) {
                const newOptions = interceptor.register(options);
                if (newOptions !== undefined) options = newOptions;
            }
        }
        return options;
    }

    withOptions(options) {
        const mergeOptions = opt =>
            Object.assign({}, options, typeof opt === "string" ? { name: opt } : opt);

        // Prevent creating endless prototype chains
        options = Object.assign({}, options, this._withOptions);
        const base = this._withOptionsBase || this;
        const newHook = Object.create(base);

        (newHook.tapAsync = (opt, fn) => base.tapAsync(mergeOptions(opt), fn)),
            (newHook.tap = (opt, fn) => base.tap(mergeOptions(opt), fn));
        newHook.tapPromise = (opt, fn) => base.tapPromise(mergeOptions(opt), fn);
        newHook._withOptions = options;
        newHook._withOptionsBase = base;
        return newHook;
    }

    isUsed() {
        return this.taps.length > 0 || this.interceptors.length > 0;
    }

    intercept(interceptor) {
        this._resetCompilation();
        this.interceptors.push(Object.assign({}, interceptor));
        if (interceptor.register) {
            for (let i = 0; i < this.taps.length; i++)
                this.taps[i] = interceptor.register(this.taps[i]);
        }
    }

    _resetCompilation() {
        this.call = this._call;
        this.callAsync = this._callAsync;
        this.promise = this._promise;
    }

    _insert(item) { // ??? 基本是插入到最后面
        this._resetCompilation();
        let before;
        if (typeof item.before === "string") before = new Set([item.before]);
        else if (Array.isArray(item.before)) {
            before = new Set(item.before);
        }
        let stage = 0;
        if (typeof item.stage === "number") stage = item.stage;
        let i = this.taps.length;
        while (i > 0) {
            i--;
            const x = this.taps[i];
            this.taps[i + 1] = x;
            const xStage = x.stage || 0;
            if (before) {
                if (before.has(x.name)) {
                    before.delete(x.name);
                    continue;
                }
                if (before.size > 0) {
                    continue;
                }
            }
            if (xStage > stage) {
                continue;
            }
            i++;
            break;
        }
        this.taps[i] = item;
    }
}

function createCompileDelegate(name, type) {
    return function lazyCompileHook(...args) {
        this[name] = this._createCall(type);
        return this[name](...args);
        // 返回的是一个匿名函数,执行该函数的话会依次执行this._x(及这里的this.taps数组里面的fn)的方法们,
    // 里面会有一些判断中断的逻辑
    };
}

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
    }
});

module.exports = Hook;

taps属性就是用来存储tap方法的参数(名称和回调,会被在tap方法内转换为形如{ name:"pluginName", type: "sync", fn: fn })的。interceptors就是来存储拦截器信息的。_x属性默认是undefined,但是在createCompileDelegate方法里面利用数组的map将taps属性的每个fn处理后赋值成_x。createCompileDelegate方法是用惰性调用_createCall,_createCall最终是调用compile方法看上面的基类会反向,基类Hook要求它的子类必须覆盖自己的compile方法,那我们看看常用的SyncHook这个子类。

// node_modules/tapable/SyncHook.js
const Hook = require("./Hook");
const HookCodeFactory = require("./HookCodeFactory");

class SyncHookCodeFactory extends HookCodeFactory {
    content({ onError, onDone, rethrowIfPossible }) {
        return this.callTapsSeries({
            onError: (i, err) => onError(err),
            onDone,
            rethrowIfPossible
        });
    }
}

const factory = new SyncHookCodeFactory();

class SyncHook extends Hook {
    tapAsync() {
        throw new Error("tapAsync is not supported on a SyncHook");
    }

    tapPromise() {
        throw new Error("tapPromise is not supported on a SyncHook");
    }

    compile(options) {
        factory.setup(this, options);
        return factory.create(options);
    }
}

module.exports = SyncHook;

它的compile方法写的比较简单,其实还是调用HookCodeFactory类的的两个方法。

"use strict";

class HookCodeFactory {
    constructor(config) {
        this.config = config;
        this.options = undefined;
        this._args = undefined;
    }

    create(options) {
        this.init(options);
        let fn;
        switch (this.options.type) {
            case "sync":
                fn = new Function(
                    this.args(),
                    '"use strict";\n' +
                        this.header() +
                        this.content({
                            onError: err => `throw ${err};\n`,
                            onResult: result => `return ${result};\n`,
                            resultReturns: true,
                            onDone: () => "",
                            rethrowIfPossible: true
                        })
                );
                break;
            case "async":
                fn = new Function(
                    this.args({
                        after: "_callback"
                    }),
                    '"use strict";\n' +
                        this.header() +
                        this.content({
                            onError: err => `_callback(${err});\n`,
                            onResult: result => `_callback(null, ${result});\n`,
                            onDone: () => "_callback();\n"
                        })
                );
                break;
            case "promise":
                let errorHelperUsed = false;
                const content = this.content({
                    onError: err => {
                        errorHelperUsed = true;
                        return `_error(${err});\n`;
                    },
                    onResult: result => `_resolve(${result});\n`,
                    onDone: () => "_resolve();\n"
                });
                let code = "";
                code += '"use strict";\n';
                code += "return new Promise((_resolve, _reject) => {\n";
                if (errorHelperUsed) {
                    code += "var _sync = true;\n";
                    code += "function _error(_err) {\n";
                    code += "if(_sync)\n";
                    code += "_resolve(Promise.resolve().then(() => { throw _err; }));\n";
                    code += "else\n";
                    code += "_reject(_err);\n";
                    code += "};\n";
                }
                code += this.header();
                code += content;
                if (errorHelperUsed) {
                    code += "_sync = false;\n";
                }
                code += "});\n";
                fn = new Function(this.args(), code);
                break;
        }
        this.deinit();
        return fn;
    }

    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() {
        let code = "";
        if (this.needContext()) {
            code += "var _context = {};\n";
        } else {
            code += "var _context;\n";
        }
        code += "var _x = this._x;\n";
        if (this.options.interceptors.length > 0) {
            code += "var _taps = this.taps;\n";
            code += "var _interceptors = this.interceptors;\n";
        }
        for (let i = 0; i < this.options.interceptors.length; i++) {
            const interceptor = this.options.interceptors[i];
            if (interceptor.call) {
                console.log(' 执行interceptor.call-> ', this.options.interceptors.length, i, this.getInterceptor(i));
                code += `${this.getInterceptor(i)}.call(${this.args({
                    before: interceptor.context ? "_context" : undefined
                })});\n`;
            }
        }
        return code;
    }

    needContext() {
        for (const tap of this.options.taps) if (tap.context) return true;
        return false;
    }

    callTap(tapIndex, { onError, onResult, onDone, rethrowIfPossible }) {
        let code = "";
        let hasTapCached = false;
        for (let i = 0; i < this.options.interceptors.length; i++) {
            const interceptor = this.options.interceptors[i];
            if (interceptor.tap) {
                if (!hasTapCached) {
                    code += `var _tap${tapIndex} = ${this.getTap(tapIndex)};\n`;
                    hasTapCached = true;
                }
                code += `${this.getInterceptor(i)}.tap(${
                    interceptor.context ? "_context, " : ""
                }_tap${tapIndex});\n`;
            }
        }
        code += `var _fn${tapIndex} = ${this.getTapFn(tapIndex)};\n`;
        const tap = this.options.taps[tapIndex];
        switch (tap.type) {
            case "sync":
                if (!rethrowIfPossible) {
                    code += `var _hasError${tapIndex} = false;\n`;
                    code += "try {\n";
                }
                if (onResult) {
                    code += `var _result${tapIndex} = _fn${tapIndex}(${this.args({
                        before: tap.context ? "_context" : undefined
                    })});\n`;
                } else {
                    code += `_fn${tapIndex}(${this.args({
                        before: tap.context ? "_context" : undefined
                    })});\n`;
                }
                if (!rethrowIfPossible) {
                    code += "} catch(_err) {\n";
                    code += `_hasError${tapIndex} = true;\n`;
                    code += onError("_err");
                    code += "}\n";
                    code += `if(!_hasError${tapIndex}) {\n`;
                }
                if (onResult) {
                    code += onResult(`_result${tapIndex}`);
                }
                if (onDone) {
                    code += onDone();
                }
                if (!rethrowIfPossible) {
                    code += "}\n";
                }
                break;
            case "async":
                let cbCode = "";
                if (onResult) cbCode += `(_err${tapIndex}, _result${tapIndex}) => {\n`;
                else cbCode += `_err${tapIndex} => {\n`;
                cbCode += `if(_err${tapIndex}) {\n`;
                cbCode += onError(`_err${tapIndex}`);
                cbCode += "} else {\n";
                if (onResult) {
                    cbCode += onResult(`_result${tapIndex}`);
                }
                if (onDone) {
                    cbCode += onDone();
                }
                cbCode += "}\n";
                cbCode += "}";
                code += `_fn${tapIndex}(${this.args({
                    before: tap.context ? "_context" : undefined,
                    after: cbCode
                })});\n`;
                break;
            case "promise":
                code += `var _hasResult${tapIndex} = false;\n`;
                code += `var _promise${tapIndex} = _fn${tapIndex}(${this.args({
                    before: tap.context ? "_context" : undefined
                })});\n`;
                code += `if (!_promise${tapIndex} || !_promise${tapIndex}.then)\n`;
                code += `  throw new Error('Tap function (tapPromise) did not return promise (returned ' + _promise${tapIndex} + ')');\n`;
                code += `_promise${tapIndex}.then(_result${tapIndex} => {\n`;
                code += `_hasResult${tapIndex} = true;\n`;
                if (onResult) {
                    code += onResult(`_result${tapIndex}`);
                }
                if (onDone) {
                    code += onDone();
                }
                code += `}, _err${tapIndex} => {\n`;
                code += `if(_hasResult${tapIndex}) throw _err${tapIndex};\n`;
                code += onError(`_err${tapIndex}`);
                code += "});\n";
                break;
        }
        return code;
    }

    // ...

    args({ before, after } = {}) {
        let allArgs = this._args;
        if (before) allArgs = [before].concat(allArgs);
        if (after) allArgs = allArgs.concat(after);
        if (allArgs.length === 0) {
            return "";
        } else {
            return allArgs.join(", ");
        }
    }

    getTapFn(idx) {
        return `_x[${idx}]`;
    }

    getTap(idx) {
        return `_taps[${idx}]`;
    }

    getInterceptor(idx) {
        return `_interceptors[${idx}]`;
    }
}

module.exports = HookCodeFactory;

它的setup方法就是执行的前面说的map的过程,而create方法就是利用new Function函数体的字符串形式来创建匿名函数并返回。这些匿名函数类似这样

(function(compilation, params
/*``*/) {
"use strict";
var _context;
var _x = this._x;
var _fn0 = _x[0];
_fn0(compilation, params);
var _fn1 = _x[1];
_fn1(compilation, params);
var _fn2 = _x[2];
_fn2(compilation, params);
var _fn3 = _x[3];
_fn3(compilation, params);
})

这里的参数compilation, params就是在构造函数的hooks里面的新建SyncHook实例时传的参数new SyncHook(["compilation", "params"])。执行这些匿名函数(断点截图中的2156、2153、2150)的就是在执行我们在tap订阅时的回调函数了。比如:

<script async src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js"></script>
<script>
     (adsbygoogle = window.adsbygoogle || []).push({
          google_ad_client: "ca-pub-3013839362871866",
          enable_page_level_ads: true
     });
</script>
// node_modules/webpack/libProgreessPlugin.js
compiler.hooks.compilation.tap("ProgressPlugin", compilation => { })  // 这里只有用了第一个参数

在截图里面我们还能看到在执行了hooks.xxx.callAsync方法之后,就开始执行匿名函数了。
我们简单总结下:tap时订阅,call是执行对应的回调。

分类: Web前端

发表评论

此站点使用Akismet来减少垃圾评论。了解我们如何处理您的评论数据