15

webpack笔记——hook执行时call的是什么

 5 years ago
source link: https://www.daozhao.com/8845.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

我们一般使用的插件都是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() 了。


Recommend

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK