深刻源码解析 tapable 实现原理

引子

若是你了解过 webpack,他们会告诉你,webpack 底层是基于 tapablejavascript

若是你好奇 tapable 是什么,你可能会看到其余地方的博客:『Tapble是webpack在打包过程当中,控制打包在什么阶段调用Plugin的库,是一个典型的观察者模式的实现』。java

可能,他们还会告诉你,Tapable的核心功能就是控制一系列注册事件之间的执行流控制,对吧?webpack

若是你了解的继续深一些,可能还会看到下面的表格,见到如此多的钩子:web

名称 钩入的方式 做用
Hook taptapAsynctapPromise 钩子基类
SyncHook tap 同步钩子
SyncBailHook tap 同步钩子,只要执行的 handler 有返回值,剩余 handler 不执行
SyncLoopHook tap 同步钩子,只要执行的 handler 有返回值,一直循环执行此 handler
SyncWaterfallHook tap 同步钩子,上一个 handler 的返回值做为下一个 handler 的输入值
AsyncParallelBailHook taptapAsynctapPromise 异步钩子,handler 并行触发,可是跟 handler 内部调用回调函数的逻辑有关
AsyncParallelHook taptapAsynctapPromise 异步钩子,handler 并行触发
AsyncSeriesBailHook taptapAsynctapPromise 异步钩子,handler 串行触发,可是跟 handler 内部调用回调函数的逻辑有关
AsyncSeriesHook taptapAsynctapPromise 异步钩子,handler 串行触发
AsyncSeriesLoopHook taptapAsynctapPromise 异步钩子,能够触发 handler 循环调用
AsyncSeriesWaterfallHook taptapAsynctapPromise 异步钩子,上一个 handler 能够根据内部的回调函数传值给下一个 handler
Hook Helper 与 Tapable 类
名称 做用
HookCodeFactory 编译生成可执行 fn 的工厂类
HookMap Map 结构,存储多个 Hook 实例
MultiHook 组合多个 Hook 实例
Tapable 向前兼容老版本,实例必须拥有 hooks 属性

那么,问题来了,这些钩子的内部是如何实现的?它们之间有什么样的继承关系? 源码设计上有什么优化地方?api

本文接下来,将从 tapable 源码出发,解开 tapable 神秘的面纱。数组

Tapable 源码核心

先上一张大图,涵盖了 80% 的 tapable 核心流程promise

上图中,咱们看到, tapable 这个框架,最底层的有两个类: 基础类 Hook, 工厂类 HookCodeFactory缓存

上面列表中 tapable 提供的钩子,好比说 SyncHookSyncWaterHooks等,都是继承自基础类 Hook闭包

图中可见,这些钩子,有两个最关键的方法: tap方法、 call 方法。并发

这两个方法是tapable 暴露给用户的api, 简单且好用。 webpack 是基于这两个api 建构出来的一套复杂的工做流。

咱们再来看工厂类 HookCodeFactory,它也衍生出SyncHookCodeFactorySyncWaterCodeFactory 等不一样的工厂构造函数,实例化出来不一样工厂实例factory

工厂实例factory的做用是,拼接生产出不一样的 compile 函数,生产 compile 函数的过程,本质上就是拼接字符串,没有什么魔法,下文中会介绍到。

这些不一样的 compile 函数,最终会在 call() 方法被调用。

呼,刚才介绍了一大堆概念,但愿没有把读者弄晕

咱们首先看一下,call 方法和 tap 方法是如何使用的。

基本用法

下面是简单的一个例子:

let hook = new SyncHook(['foo']);

hook.tap({
    name: 'dudu',
    before: '',
}, (params) => {
    console.log('11')
})

hook.tap({
    name: 'lala',
    before: 'dudu',
}, (params) => {
    console.log('22')
})

hook.tap({
 name: 'xixi',
 stage: -1
}, (params) => {
 console.log('22')
})

hook.call('tapable', 'learn')
复制代码

上面代码的输出结果:

// 22
// 11
复制代码

咱们使用 tap()方法用于注册事件,使用 call() 来触发全部回调函数执行。

注意点:

  • 在实例化 SyncHook 时,咱们传入字符串数组。数组的长度很重要,会影响你经过 call 方法调用 handler 时入参个数。就像例子所示,调用 call 方法传入的是两个参数,实际上 handler 只能接收到一个参数,由于你在new SyncHook 的时候传入的字符串数组长度是1。

  • 经过 tap 方法去注册 handler 时,第一个参数必须有,格式以下:

    interface Tap {
            name: string,  // 标记每一个 handler,必须有,
            before: string | array, // 插入到指定的 handler 以前
            type: string,   // 类型:'sync', 'async', 'promise'
            fn: Function,   // handler
            stage: number,  // handler 顺序的优先级,默认为 0,越小的排在越前面执行
            context: boolean // 内部是否维护 context 对象,这样在不一样的 handler 就能共享这个对象
    }
    复制代码

    上面参数,咱们重点关注 beforestage,这两个参数影响了回调函数的执行顺序 。上文例子中, name'lala'handler 注册的时候,是传了一个对象,它的 before 属性为 dudu,说明这个 handler 要插到 nameduduhandler 以前执行。可是又由于 namexixihandler 注册的时候,stage 属性为 -1,比其余的 handlerstage 要小,因此它会被移到最前面执行。

那么,tapcall是如何实现的呢? 被调用的时候,背后发生了什么?

咱们接下来,深刻到源码分析 tapable 机制。

下文中分析的源码是 tapable v1.1.3 版本

tap 方法的实现

上文中,咱们在注册事件时候,用了 hook.tap() 方法。

tap 方法核心是,把注册的回调函数,维护在这个钩子的一个数组中。

tap 方法实如今哪里呢?

代码里面,hookSyncHook 的实例,SyncHook又继承了 Hook 基类,在 Hook 基类中,具体代码以下:

class Hook {
    tap(options, fn) {
        options = this._runRegisterInterceptors(options);
        this._insert(options);
    }
}
复制代码

咱们发现,tap 方法最终调用了_insert方法,

_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);
        }
        // 默认 stage是0
        // stage 值越大,
        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;
    }
复制代码

把注册的方法,都 push 到一个 taps 数组上面。这里对 beforestage 作了处理,使得 push 到 taps 数组的顺序不一样,从而决定了 回调函数的执行顺序不一样。

call 方法的实现

SyncHook.js 中,咱们没有找到 call 方法的定义。再去 Hook 基类上找,发现有这样一句, call 方法 是 _call 方法

this.call = this._call;
复制代码
class Hook {
    construcotr {
        // 这里发现,call 方法就是 this._call 方法
        this.call = this._call;
    }
    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
        });
    }
}
复制代码

那么, _call 方法是在哪里定义的呢?看下面, this._callcreateCompileDelegate("call", "sync")的返回值。

Object.defineProperties(Hook.prototype, {
    // this._call 是 createCompileDelegate("call", "sync") 的值, 为函数 lazyCompileHook
    _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
    }
});
复制代码

接着往下看 createCompileDelegate 方法里面作了什么?

// 下面的createCompileDelegate 方法 返回了一个新的方法,
// 参数 name 是闭包保存的字符串 'call'
function createCompileDelegate(name, type) {
    return function lazyCompileHook(...args) {
        // 实际上
        // this.call = this._creteCall(type)
        // return this.call()
        this[name] = this._createCall(type);
        return this[name](...args);
    };
}
复制代码

上面的代码,createCompileDelegate 先调用 this._createCall() 方法,把返回值赋值给 this[name]

this._createCall() 里面本质是调用了this.compiler 方法,可是基类Hook上的compiler() 方法是一个空实现,顺着这条线索找下来,这是一条死胡同。

this.compiler 方法,真正是定义在衍生类 SyncHook上,也就是在 SyncHook.js 中,SyncHook 类从新定义了 compiler 方法来覆盖:

const factory = new SyncHookCodeFactory();
class SyncHook extends Hook {
    compile(options) {
        factory.setup(this, options);
        return factory.create(options);
    }
}
复制代码

这里的 factory ,就是本文开头提到的工厂实例。factory.create 的产物以下:

ƒ anonymous() {
  "use strict";
  var _context;
  var _x = this._x;
  var _fn0 = _x[0];
  _fn0();  
  var _fn1 = _x[1];
  _fn1();
}
复制代码

this._x 是一个数组,里面存放的就是咱们注册的 taps 方法。上面代码的核心就是,遍历咱们注册的 taps 方法,并去执行。

factory.create 的核心是,根据传入的type 类型,拼接对应的字符串,代码以下:

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
    })
);
复制代码

上面代码中, content 方法是定义在 SyncHook 的衍生类上的,

class SyncHookCodeFactory extends HookCodeFactory {
    // 区分不一样的类型的 工程
    // content 方法用于拼接字符串
    // HookCodeFactory 里面会调用 this.content(), 访问到的是这里的 content
    content({ onError, onDone, rethrowIfPossible }) {
        return this.callTapsSeries({
            onError: (i, err) => onError(err),
            onDone,
            rethrowIfPossible
        });
    }
}
复制代码

到这里为止一目了然,咱们能够看到咱们的注册回调是怎样在this.call方法中一步步执行的。

在这里的优化, tapable 用到了《javascript 高级程序设计》中的『惰性函数』,缓存下来 this.__createCall call,从而提高性能

惰性函数

什么是惰性函数? 惰性函数有什么做用?

基类Hook上的compiler 方法是一个空实现,具体实现是 衍生类 上

compile 传入的参数很丰富

return this.compile({
 taps: this.taps,
 interceptors: this.interceptors,
 args: this._args,
 type: type
 });
复制代码

工厂的产物

Tapable有一系列Hook方法,可是这么多的Hook方法都是无非是为了控制注册事件的执行顺序以及异常处理

最简单的SyncHook前面已经讲过,咱们从SyncBailHook开始看。

SyncBailHook

这类钩子的特色是,判断 handler 的返回值,是否===undefined, 若是是 undefined , 则执行,若是有返回值,则 return 返回值

// fn, 调用 call 时,实际执行的代码
function anonymous(/*``*/) {
 "use strict";
	var _context;
	var _x = this._x;
	var _fn0 = _x[0];
	var _result0 = _fn0();
	if (_result0 !== undefined) {
		return _result0;
	} else {
		var _fn1 = _x[1];
		var _result1 = _fn1();
		if (_result1 !== undefined) {
			return _result1;
		} else {
		}
	}
}
复制代码

经过打印fn,咱们能够轻易的看出,SyncBailHook提供了停止注册函数执行的机制,只要在某个注册回调中返回一个非undefined的值,运行就会停止。

SyncWaterfallHook

function anonymous(arg1) {
 "use strict";
	var _context;
	var _x = this._x;
	var _fn0 = _x[0];
	var _result0 = _fn0(arg1);
	if (_result0 !== undefined) {
		arg1 = _result0;
	}
	var _fn1 = _x[1];
	var _result1 = _fn1(arg1);
	if (_result1 !== undefined) {
		arg1 = _result1;
	}
	return arg1;
}
复制代码

能够看出SyncWaterfallHook就是将上一个事件注册回调的返回值做为下一个注册函数的参数,这就要求在new SyncWaterfallHook(['arg1']);须要且只能传入一个形参。

SyncLoopHook

// 打印fn
function anonymous(arg1) {
 "use strict";
	var _context;
	var _x = this._x;
	var _loop;
	do {
		_loop = false;
		var _fn0 = _x[0];
		var _result0 = _fn0(arg1);
		if (_result0 !== undefined) {
			_loop = true;
		} else {
			var _fn1 = _x[1];
			var _result1 = _fn1(arg1);
			if (_result1 !== undefined) {
				_loop = true;
			} else {
				if (!_loop) {
				}
			}
		}
	} while (_loop);
}
复制代码

SyncLoopHook只有当上一个注册事件函数返回undefined的时候才会执行下一个注册函数,不然就不断重复调用。

AsyncSeriesHook

Series有顺序的意思,这个Hook用于按顺序执行异步函数。

function anonymous(_callback) {
 "use strict";
	var _context;
	var _x = this._x;
	var _fn0 = _x[0];
	_fn0(_err0 => {
		if (_err0) {
			_callback(_err0);
		} else {
			var _fn1 = _x[1];
			_fn1(_err1 => {
				if (_err1) {
					_callback(_err1);
				} else {
					_callback();
				}
			});
		}
	});
}
复制代码

从打印结果能够发现,两个事件以前是串行的,而且next中能够传入err参数,当传入err,直接中断异步,而且将err传入咱们在call方法传入的完成回调函数中。

AsyncParallelHook

asyncParallelHook 是异步并发的钩子,适用场景:一些状况下,咱们去并发的请求不相关的接口,好比说请求用户的头像接口、地址接口。

factory.create 的产物是下面的字符串

function anonymous(_callback) {
 "use strict";
    var _context;
    var _x = this._x;
    do {
        // _counter 是 注册事件的数量
        var _counter = 2;
        var _done = () => {
            _callback();
        };

        if (_counter <= 0) break;

        var _fn0 = _x[0];

        _fn0(_err0 => {
            // 这个函数是 next 函数
            // 调用这个函数的时间不能肯定,有可能已经执行了接下来的几个注册函数
            if (_err0) {
                // 若是还没执行全部注册函数,终止
                if (_counter > 0) {
                    _callback(_err0);
                    _counter = 0;
                }
            } else {
                // 检查 _counter 的值,若是是 0 的话,则结束
                // 一样,因为函数实际调用时间没法肯定,须要检查是否已经运行完毕,
                if (--_counter === 0) {
                    _done()
                };
            }
        });

        // 执行下一个注册回调以前,检查_counter是否被重置等,若是重置说明某些地方返回err,直接终止。
        if (_counter <= 0) break;

        var _fn1 = _x[1];

        _fn1(_err1 => {
            if (_err1) {
                if (_counter > 0) {
                    _callback(_err1);
                    _counter = 0;
                }
            } else {
                if (--_counter === 0) _done();
            }
        });

    } while (false);
}
复制代码

从打印结果看出Event2的调用在AsyncCall in Event1以前,说明异步事件是并发的。

相关文章
相关标签/搜索