webpack筆記——hook執行時call的是什麼

談笑斗酒發表於2020-03-28

原文地址:道招網webpack筆記——hook執行時call的是什麼

我們一般使用的外掛都是Hook子類,比如SyncHook,沒有複雜的重寫基類Hook的compile方法 先看Hook基類

// 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;
複製程式碼

需要注意這裡的this.call,this.promise,this.callAsync

// 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例項的create的結果

// node_modules/tapable/HookCodeFactory.js
"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;
複製程式碼

所以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);
})
複製程式碼

在使用外掛時我們會對應的方法。 以this.call為例,它的本質是執行this._call,也就是createCompileDelegate後的lazyCompileHook。在呼叫this.call()時,返回的是this._createCall(type)(...arg)的結果,也就是上面的new Function()了。

相關文章