看啥推荐读物
专栏名称: 雾豹
高级前端开发
目录
相关文章推荐
今天看啥  ›  专栏  ›  雾豹

深入源码解析 tapable 实现原理

雾豹  · 掘金  ·  · 2019-11-05 15:17
阅读 15

深入源码解析 tapable 实现原理

引子

如果你了解过 webpack,他们会告诉你,webpack 底层是基于 tapable

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

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

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

名称 钩入的方式 作用
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 属性

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

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

Tapable 源码核心

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

上图中,我们看到, 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之前,说明异步事件是并发的。




原文地址:访问原文地址
快照地址: 访问文章快照