【webpack源码4】- tapable模块

webpack | 2020-06-13 16:20:02 294次 0次

tapable 是一个单独的模块,上一篇中介绍了它如何使用,简单说就是做了一些事件的监听,实现发布订阅功能。在开篇之前先说明下这个工具的实现在之前的版本中是直接通过编写抽象函数逻辑实现功能,而现在的版本中则是通过 new Function 的方式通过字符拼接生成特定代码逻辑实现功能,这么做的目的是为了一些优化,尽管使用构造函数定义函数代价是昂贵的,但是,tapable 生成的代码是单形态的,这样运行效率其实是更高效的。具体可以参考这里(issues)

const f = (...fns) => (...args) => {
  for (const fn of fns) {
    fn(...args)
}}
// g 性能高于上面的
const g = (a, b, c) => (x, y) => {
  a(x, y)
  b(x, y)
  c(x, y)
}

一、SyncHook子类

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;

如上代码我们可以看到 SyncHook 类继承了 Hook 类,这个父类中主要做了一些初始化操作,定义相关事件触发方式;然后定义了 SyncHookCodeFactory 类继承 HookCodeFactory 类,它主要是定义不同的调用方式和参数生成相应的代码片段。

其中 SyncHook 为一个同步方法,如果是 tapAsync tapPromise 方式来触发则会抛出错误,其中的 compile 方法会在父类中被触发。SyncHookCodeFactory 中的 content 是在 create 之后被调用。


二、Hook父类之初始化

constructor 中初始化一些变量:

    constructor(args) {
        if (!Array.isArray(args)) args = [];
        this._args = args; //参数 new 的时候传进来的一系列参数.
        this.taps = [];    //taps绑定事件集合
        this.interceptors = [];   //拦截器集合
        this.call = this._call;   //同步触发方式
        this.promise = this._promise;  //异步 promise 触发
        this.callAsync = this._callAsync; //异步触发
        // 用于调用函数的时候,保存钩子数组的变量
        this._x = undefined;
    }

这里先给父类挂载上 call 方法,这样 new 子类之后调用的 .call 方法后,会先找子类,此时子类中没有这个方法则调用父类的。那么就需要看下 _call 这个方法是怎么被定义的:

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

它的 value 值为 createCompileDelegate 函数,将当前的调用名和类型传入:

function createCompileDelegate(name, type) {
    return function lazyCompileHook(...args) {
        //子类调用时,this默认绑定到子类
        this[name] = this._createCall(type);
        return this[name](...args);
    };
}

返回的 lazyCompileHook 会最终在我们显示调用 syncHook.call("Mikeqq", 28); 时候被触发,此时的函数中 this 指向子类,并给子类挂载上 call 方法,继续看下 _createCall 这个父类中的方法:

compile(options) {
    throw new Error("Abstract: should be overriden");
}
_createCall(type) {
    // compile方法 必须每个子类实现 否则提示错误 this指向子类
    let res = this.compile({
        taps: this.taps,
        interceptors: this.interceptors,
        args: this._args,
        type: type
    });
    return res
}

这里面的 this 指向的是子类,父类中 compile 这个方法直接抛出错误,则说明这个方法必须由子类来实现。在上面子类代码中可以看到它里面主要是调用了 SyncHookCodeFactory  中的一些方法,这个等后面再继续分析。

这里优先看一下 _resetCompilation 方法,用来重置资源,因为我们有可能注册 tap 事件后执行了一次 call 方法,然后下面又继续编写了 tap 注册函数再执行触发,这样因为 createCompileDelegate 函数中的闭包缓存了历史已经挂载好的 call 方法,导致出现错误,所以需要重置

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


三、Hook父类之tap

在上面介绍了初始的一些定义,然后我们实例化一个对象后会先执行 tap 事件注册:

const syncHook = new SyncHook(["name", "age"]);
// 注册事件
syncHook.tap("frist", (name, age) => {
  ...
});
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 = { type: "sync", fn: fn, name: options }; 
    //然后注册拦截器
    options = this._runRegisterInterceptors(options);
    // 插入钩子
    this._insert(options);
}

在执行 tap 注册时候,会先注册并执行拦截器:

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

可以看到这个方法读取了 interceptors 属性,并且如果存在就触发里面的 register,传入旧的配置项获取返回新的配置,所以参考上篇的注册器,它的触发时期是在 tap 事件注册时候就会被触发。interceptors 是通过手动调用执行的:

intercept(interceptor) {
    //重置资源
    this._resetCompilation();
    //记录拦截器
    this.interceptors.push(Object.assign({}, interceptor));
    //intercept有可能在tap事件 之上、之下、之间 保证无论怎样写,都能正确调用执行
    if (interceptor.register) {
        for (let i = 0; i < this.taps.length; i++)
            this.taps[i] = interceptor.register(this.taps[i]);
    }
}

发现这里面又调用了一次 register,因为有可能先创建了 tap 事件注册,再创建 intercept 拦截器,和上面那段代码的区别是上方保证先创建了拦截器,再执行 tap 注册执行拦截器注册函数,无论怎样,都是保证拦截器中的注册函数能够正确在 tap 事件注册之后执行。

最后注册事件中调用 _insert 方法,用来进行注册事件的排序操作,最终保证 this.taps 中事件顺序。注册事件时候是可以进行一个顺序控制的,详情查看测试用例

hook.tap(
    {
        name: "B",
        before: "A" 
    },
    () => calls.push("B")
);
["B", "A"]

before也可以是个数组
hook.tap(
    {
        name: "C",
        before: ["A", "B"]
    },
    () => calls.push("C")
);
["C", "B", "A"]
_insert(item) {
    // 重置资源,比如call之后又注册了 tap 事件,最后再调用一次call  如果不重置,则闭包了第一次call的那个结果
    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);
    }
    //控制顺序的属性,值越小,执行得就越在前面 而且优先级低于 before
    let stage = 0;
    if (typeof item.stage === "number") stage = item.stage;
    let i = this.taps.length;
    //这里之所以用while 是因为需要对 stage 或 before等配置进行排序
    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恢复 注意直接跳出了while
        i++;
        break;
    }
    //得到最终排序后的taps数组
    this.taps[i] = item;
}

这个排序算法太优秀,跟着测试用例梳理一下就会相对清晰,不考虑 before stage 的情况下,就是往数组中不断的追加,但是是加上 before 之后的控制就明白了这里选择 while 遍历控制起来会方便更多。

hook 父类主要就是做了时间排序,初始化等工作,继续看另一个类执行真正的代码生成。


四、HookCodeFactory

执行 call 之后会在 Hook 父类中最终调用子类的 compile,这里面执行了factory setup creat 方法。

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

setup 方法仅仅将 tap 注册的钩子挂载到子类的 _x 属性上,主要的是 create 方法,它判断了同步、异步、异步 promise 三种调用方式,来生成不同的代码片段,本次主要看同步的模式:

create(options) {
    //初始化 给子类:this挂载 options 和 args属性
    this.init(options);
    let fn;
    switch (this.options.type) {
        case "sync":
            fn = new Function(
                this.args(),  //在注册tap时传入的参数
                '"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":
           ...
        case "promise":
           ...
    }
    //释放 和上面的init作用相反
    this.deinit();
    return fn;
}

在开篇简要解释了为什么使用 new Function 这种方式,首先构造这个函数先传入一些参数,调用 this.args,返回一个参数增强字符串,比如添加 context,异步的代码中添加一些回调处理等:

args({ before, after } = {}) {
    let allArgs = this._args;
    //context放在参数第一位
    if (before) allArgs = [before].concat(allArgs);
    //回调的一些逻辑
    if (after) allArgs = allArgs.concat(after);
    if (allArgs.length === 0) {
        return "";
    } else {
        return allArgs.join(", ");
    }
}

接下来执行 header 方法,判断是否有 context(可传入 true)字段   判断是否有拦截器,拦截器中如果有 call 则需要在手动执行 call 之前调用,并传入 context,这个字段如果传入了则就是一个全局对象,各个钩子中都可以通过第一个参数使用获取:

header() {
    let code = "";
    //判断是否传入 context,Hook中有参数转换
    if (this.needContext()) {
        code += "var _context = {};\n";
    } else {
        code += "var _context;\n";
    }
    //这个this代表 谁调用指向谁 例如:new SyncHook()时指向这个生成的实例 和 
    //上面setup方法中接受的instance参数一致
    code += "var _x = this._x;\n";  
    if (this.options.interceptors.length > 0) {
        code += "var _taps = this.taps;\n";
        code += "var _interceptors = this.interceptors;\n";
    }
    //拦截器遍历并判断call是否存在,如果存在则优先调用
    for (let i = 0; i < this.options.interceptors.length; i++) {
        const interceptor = this.options.interceptors[i];
        if (interceptor.call) {
            //getInterceptor方法就是获取拦截器
            code += `${this.getInterceptor(i)}.call(${this.args({
                before: interceptor.context ? "_context" : undefined
            })});\n`;
        }
    }
    return code;
}

注意拦截器中的 call 方法传入的参数是一个变量,到这里看到的是 h = new SyncHook(['xxx']); 传入的这个参数,但是最终执行时这个参数是 h1.call("test1111"); 这里传入的最终实参,因为使用构造函数时传入的形参就是实例化时传入的参数(这个参数没有实际意义,只是一个标记),这个稍后看下最终生成的代码就明白了。

接下来调用了 content,定义在子类中,然后触发 callTapsSeries(父类中)方法,上面主要是进行了初始变量、拦截器等一些代码创建,这个方法则是根据用户的钩子创建如何执行这些逻辑:

callTapsSeries({
    onError,
    onResult,
    resultReturns,
    onDone,
    doneReturns,
    rethrowIfPossible
}) {
    //onDone返回一个字符串
    if (this.options.taps.length === 0) return onDone();
    //异步的方式
    const firstAsync = this.options.taps.findIndex(t => t.type !== "sync");
    const somethingReturns = resultReturns || doneReturns || false;
    let code = "";
    let current = onDone;
    //[a, b, e]
    for (let j = this.options.taps.length - 1; j >= 0; j--) {
        const i = j;
        ...
        const done = current;
        const doneBreak = skipDone => {
            if (skipDone) return "";
            return onDone();
        };
        // 处理单个taps对象
        const content = this.callTap(i, {
            onError: error => onError(i, error, done, doneBreak),
            onResult:
                onResult &&
                (result => {
                    return onResult(i, result, done, doneBreak);
                }),
            onDone: !onResult && done,
            rethrowIfPossible:
                rethrowIfPossible && (firstAsync < 0 || i < firstAsync)
        });
        //直到遍历到最后一个  将前面方法全覆盖了  只要最终结果
        current = () => content;
    }
    code += current();
    return code;
}

到这里基本上开始了神仙一般的代码设计,我们现在只知道了为什么这么设计,但是这个模式的创建只有作者自己知道了,这里只能简要的跟着去读一下,上面主要是调用了 callTap 这个方法:

//tapIndex 倒序 i, i - 1, i - 2, ... 0;
callTap(tapIndex, { onError, onResult, onDone, rethrowIfPossible }) {
    let code = "";
    let hasTapCached = false;
    //intercept中如果注册了 tap 则手动call执行后 先执行拦截器中的tap钩子  再执行普通 tap 中注册的函数
    for (let i = 0; i < this.options.interceptors.length; i++) {
        const interceptor = this.options.interceptors[i];
        if (interceptor.tap) {
            if (!hasTapCached) {
                //var _tap0 = _taps[0]
                code += `var _tap${tapIndex} = ${this.getTap(tapIndex)};\n`;
                hasTapCached = true;
            }
            //如果配置了context: true   则会在每个函数中第一个参数设置为这个
            //(一个对象,可以在所有钩子中set value && get value)
            //注意第二个参数将当前对应的tap对象传了进去
            code += `${this.getInterceptor(i)}.tap(${
                interceptor.context ? "_context, " : ""
            }_tap${tapIndex});\n`;
        }
    }
    //确保先执行拦截器中的tap
    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":
            ...
            break;
        case "promise":
            ....
    }
    return code;
}

可以打印看下最终返回的 code,依据上一篇的拦截器部分代码:

var _tap0 = _taps[0];
_interceptors[0].tap(_tap0);
var _fn0 = _x[0];
_fn0(xxx);
var _tap1 = _taps[1];
_interceptors[0].tap(_tap1);
var _fn1 = _x[1];
_fn1(xxx);
var _tap2 = _taps[2];
_interceptors[0].tap(_tap2);
var _fn2 = _x[2];
_fn2(xxx);
var _tap3 = _taps[3];
_interceptors[0].tap(_tap3);
var _fn3 = _x[3];
_fn3(_context, xxx);
var _tap4 = _taps[4];
_interceptors[0].tap(_tap4);
var _fn4 = _x[4];
_fn4(xxx);

最后这两段代码确实是只能简单读一读了,如果想了解具体逻辑实现最好还是去看一下它最早的版本,这个版本虽然代码逻辑不是很明显,但是可以简单的学下作者的皮毛,如何设计,如何规划代码,起码这个基类设计的完整之后,其他的 taps 子类的实现就简单的很多了,只需要简单的继承传入一些配置即可。下一篇开始 compiler 的分析。


0人赞

分享到: