深入剖析tapable源码

tapable是webpack插件机制的核心,它的作者就是webpack的作者Tobias Koppers。tapable的使用我就不再赘述了,官网上有完善的资料,我这里重点从源码的角度分析一下tapable是如何实现的。

基本流程

我们首先通过一段SyncHook的示例代码来看看tapable最基本的执行流程是什么样子的。先上示例代码:

// debug/tapable-debug.js
const {
    SyncHook,
} = require("tapable");

class Test {
    constructor() {
        this.hooks = {
            abc: new SyncHook(['param1', 'param2']),
        };
    }

    tap() {
        this.hooks.abc.tap('tap1', function (param1, param2) {
            console.log('tap1', param1, param2);
        });

        this.hooks.abc.tap('tap2', function (param1, param2) {
            console.log('tap2', param1, param2);
        });

        this.hooks.abc.tap('tap3', function (param1, param2) {
            console.log('tap3', param1, param2);
        });

        return this;
    }

    call() {
        let res = this.hooks.abc.call('value1', 'value2');
        console.log('res', res);
        return this;
    }
}

const test = new Test();
test.tap().call();

这段代码的执行结果很简单,如下所示:

// tap1 value1 value2
// tap2 value1 value2
// tap3 value1 value2

整个流程可以分为三步:
第一步:实例化SyncHook对象(new SyncHook(['param1', 'param2'])
第二步:调用tap挂载钩子函数
第三步:调用call触发钩子函数

我们一步一步来看,首先来看第一步:实例化SyncHook对象。

第一步:实例化SyncHook对象

// node_modules/tapable/lib/SyncHook.js
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);
    }
}

可以看到SyncHook自己没有构造函数,所以实际上调用的是Hook的构造函数,我们接着看Hook的构造函数都做了些什么(留意一下上面的SyncHookCodeFactory部分,我们一会儿再回过头来看这部分的代码)。

// node_modules/tapable/lib/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;
    }
    ...
}

Hook的构造函数里我们先重点关注四个信息,分别是:

  • this._args:保存形参数组,在这里就是['param1', 'param2']
  • this.taps:保存接下来通过test.tap()挂载的钩子函数的相关信息
  • this.call:这里会保存最后执行的Boss函数(划~重~点~)
  • this._x:这里会保存通过tap挂载的钩子函数的函数本体

第一步构造函数到这里就结束了,接下来我们来看第二步:调用tap挂载钩子函数。

第二步:调用tap挂载钩子函数

// node_modules/tapable/lib/Hook.js
class Hook {
    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);
    }

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

无关代码有点多,我们目前只需要关注下面精简后的部分就可以了:

// node_modules/tapable/lib/Hook.js
class Hook {
    tap(options, fn) {
        if (typeof options === "string") options = { name: options };
        options = Object.assign({ type: "sync", fn: fn }, options);
        this._insert(options);
    }
    ...
    _insert(item) {
        this.taps[i] = item;
    }
}

是不是感觉一下子清晰多了?可以看到tap做的事情其实也很简单,就是把钩子函数的相关信息options保存到this.taps数组里头。所以当test.tap()调用完毕后,this.hooks.abc.taps中的信息就是这样的:

this.hooks.abc.taps

接下来就是我们的重头戏了,第三步:调用call触发钩子函数。

第三步:调用call触发钩子函数

我们首先要问一个问题,这个call函数是从哪里来的呢?还记得Hook的构造函数嘛~

// node_modules/tapable/lib/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;
    }
}

function createCompileDelegate(name, type) {
    return function lazyCompileHook(...args) {
        this[name] = this._createCall(type); // 此时的name就是"call"
        return this[name](...args); // 此时的args就是['value1', 'value2']
    };
}

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

找到了!原来在Hook.prototype上已经定义好了_call函数,而这个_call函数的值就是createCompileDelegate返回的lazyCompileHook函数,在Hook的构造函数中就把this._call赋值给了this.call。那么当我们执行this.call函数的时候,实际上就走到了lazyCompileHook函数里头。从lazyCompileHook函数的代码来看,this.call被赋值为this._createCall(type)的返回值(注意!它就是我们的终极Boss函数),然后通过this[name](...args)立即执行该函数。
这里我们先不着急深入this._createCall(type)函数,我们先看看终极Boss函数长啥样儿再说:

// 哒~哒~哒~哒~
function anonymous(param1, param2) {
    'use strict';
    var _context;
    var _x = this._x;
    var _fn0 = _x[0];
    _fn0(param1, param2);
    var _fn1 = _x[1];
    _fn1(param1, param2);
    var _fn2 = _x[2];
    _fn2(param1, param2);
}

这里的_fn0、_fn1、_fn2就是我们通过tap挂载的三个钩子函数,而param1param2的值就是我们通过this.hooks.abc.call('value1', 'value2')传入的'value1''value2',再结合我们的示例代码一起看的话有没有一种豁然开朗的感觉呢:)
好的!接下来我们就深入this._createCall(type)函数来看看这个终极Boss函数是如何生成的吧~

// node_modules/tapable/lib/Hook.js
class Hook {
    compile(options) {
        throw new Error("Abstract: should be overriden");
    }

    _createCall(type) {
        return this.compile({
            taps: this.taps,
            interceptors: this.interceptors,
            args: this._args,
            type: type
        });
    }
}

可以看到this._createCall(type)调用了this.compile,而Hook中的compile函数是直接抛错的,从错误信息我们可以看到作者提示了我们这个compile函数是需要被继承Hook的子类复写的,在我们这里就是SyncHook,所以我们继续走到SyncHook中去看看它的compile函数是如何实现的:

// node_modules/tapable/lib/SyncHook.js
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);
    }
}

可以看到,SyncHookcompile函数中的第一行代码是调用factory也就是SyncHookCodeFactorysetup方法,而上面的SyncHookCodeFactory继承HookCodeFactory时只复写了一个content函数,那么我们自然能想到这个setup方法应该是位于HookCodeFactory里面的。事实就是如此,继续往里走,看看下面的setup方法,这里的instance就是我们的this.hooks.abc,所以你就找到了我们的钩子函数是在哪儿赋值给this._x的。继续回到compile函数看看factory.create(options)都干了些什么:

// node_modules/tapable/lib/HookCodeFactory.js
class HookCodeFactory {
    constructor(config) {
        this.config = config;
        this.options = undefined;
        this._args = undefined;
    }

    create(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;
        }
        return fn;
    }

    setup(instance, options) {
        instance._x = options.taps.map(t => t.fn);
    }
}

上面是精简过之后的代码,而factory.create(options)返回的new Function(...)就是我们一直心心念念的终极Boss函数(anonymous)啦~~
其他部分我先略过,直接看this.content函数,这个函数会回到我们的SyncHookCodeFactory类中,然后通过调用HookCodeFactorycallTapsSeries函数来生成并返回我们的终极Boss函数的函数体字符串。
后面的部分我就不展开细讲了,因为只是一些根据Hook的类型不同走不同的函数体字符串拼接逻辑罢了,这部分内容说也很难说清楚,所以还是有赖于读者自己debug进去走一走看一看,整个过程就会比较清楚了。

特大喜讯!特大喜讯!

告诉读者朋友们一个特大好消息!你只要把上面讲的SyncHook的整个流程搞清楚了,那么下面的tapable的所有Hook的流程你就都搞清楚了!

const {
    SyncHook,
    SyncBailHook,
    SyncWaterfallHook,
    SyncLoopHook,
    AsyncParallelHook,
    AsyncParallelBailHook,
    AsyncSeriesHook,
    AsyncSeriesBailHook,
    AsyncSeriesWaterfallHook,
    AsyncSeriesLoopHook,
} = require("tapable");

因为,其实其他的Hook的代码跟SyncHook的结构是一模一样的,包括构造函数和tap挂载钩子函数的部分都一样(因为实际上走的都是共同的父类Hook的逻辑),只有各自的HookCodeFactorycontent函数部分的实现不一样罢了,不信的话你看看SyncHookSyncBailHook的代码对比:

SyncHook跟SyncBailHook的代码对比

再看看SyncHookAsyncParallelHook的代码对比:
SyncHook跟AsyncParallelHook的代码对比

所以,有木有一种一箭N雕的中了大奖的感觉呢:)

写在最后

到这里,tapable的基本流程就讲完了。我们现在知道了原来tapable在调用call的时候会通过new Function(...)来实时生成一个函数执行我们挂载的钩子函数,也知道了不同的Hook之间其实主要就是生成终极Boss函数的HookCodeFactory.content函数的实现有差异罢了。其实,tapable还提供了共享上下文context对象、以及为所有的Hook都提供了intercept拦截器的功能,不过这块内容就交给聪明的读者你自己去👀啦(小小的透露一下,差别其实也还是在HookCodeFactory.content函数里面哦)。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 195,783评论 5 462
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 82,360评论 2 373
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 142,942评论 0 325
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,507评论 1 267
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,324评论 5 358
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,299评论 1 273
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,685评论 3 386
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,358评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,652评论 1 293
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,704评论 2 312
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,465评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,318评论 3 313
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,711评论 3 299
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,991评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,265评论 1 251
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,661评论 2 342
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,864评论 2 335

推荐阅读更多精彩内容