这段时间没更新文章,把时间全耗在了 tabable 核心原理分析上,如今终于把 tabable 搞懂了,经过本文分享给你们。最后我发现了一个很是容易掌握 tabable 的方法。javascript
通过这几天的学习,我把 tapable 总结成一句话「经过一种机制,监听特定的事件」。拿最经典的一个例子音频播放器来讲,核心播放服务只有一个,而须要监听播放事件的对象有不少,好比 mini播放器,主播放器,这两个播放器都要监听音频播放进度、播放开始、播放暂停事件。下图中的两个播放器都要监听播放进度:html
遇到这种需求,大多数人的作法是采用「发布订阅模式」,使用 tapable 也能解决相似这种事件监听的业务,可是它更灵活,能作更多的业务,好比 hook 函数数据之间传递,异步 hook,hook 流处理。这是在我第一次了解到 asyncHook,syncHook,syncBailHook、syncWaterfallHook 这种编程思想。固然 tapable 终究服务于 webpack,不少场景都是为 webpack 的设计而考虑的。前端
tapable 的核心类有以下几个,主要分为同步 hook 和异步 hook:java
能够经过 3 种方式来添加「事件监听函数」,也就说若是想监听某个事件,直接经过下面这几个方法来添加便可:webpack
options 能够是对象或字符串,fn 为同步回调函数。可用于全部类型的 hook,包含 sync 类型和 async 类型。web
hook.tap('SuyanSyncHook', (name, age) => {
console.log(`syncHook name: ${name}, age: ${age}`);
});
复制代码
经过 call 方法来触发事件:syncHook.call('suyan', 20);
编程
监听函数最后一个参数为一个回调函数,这个函数不能用于 sync 类型的 hook。数组
asyncHook.tapAsync('SuyanasyncSeriesHook', (source, callback) => {
setTimeout(() => {
console.log(`source3: ${source}`);
callback();
}, 2000);
});
复制代码
经过 callAsync 来触发事件:promise
asyncSeriesHook.callAsync('关注公众号素燕', ret => {
console.log(`ret = ${ret}`);
});
复制代码
须要返回一个 promise 对象;不能用于 sync 类型的 hook。网络
asynchook.tapPromise('SuyanasyncParallelHook', (source) => {
return new Promise((resolve, reject) => {
setTimeout(() => {
console.log(`source2: ${source}`);
resolve(`${source},素燕`);
}, 1000);
});
});
复制代码
经过 promise 来触发事件:
asyncHook.promise('关注公众号素燕').then(ret => {
console.log(`ret = ${ret}`);
});
复制代码
咱们下面来介绍各类 Hook 的做用,总共有两类 Hook,第一类是同步 Hook。
同步 Hook,hook 事件之间互不干扰。下面是使用 SyncHook 的例子,向 syncHook 中添加了两个回调函数,当调用 call 函数的时候,先前经过 tap 添加的回调函数将被执行:
const SyncHook = require('./lib/SyncHook');
// 建立一个 SyncHook,后面跟一个参数列表
const syncHook = new SyncHook(['name', 'age']);
// 添加一个 hook 事件
syncHook.tap('SuyanSyncHook', (name, age) => {
console.log(`syncHook name: ${name}, age: ${age}`);
});
// 添加一个 hook 事件
syncHook.tap('SuyanSyncHook', (name, age) => {
console.log(`1syncHook name: ${name}, age: ${age}`);
});
// 调用 hook
syncHook.call('suyan', 20);
复制代码
这是咋么作到的呢?咱们经过源码来分析下 tapable 的核心原理,理解了其中一个原理,其它的 Hook 能够用一样的方式来理解。
全部的 hook 都继承自 Hook 这个类,在我看来它主要作了 3 件事:
提供调用函数 call、promise、callAsync; 保存函数执行时的上下文,好比函数参数、监听者; 拦截器处理; 咱们看一下它的构造函数 constructor
constructor(args) {
// 一个数组,用来保 hook 的参数
if (!Array.isArray(args)) args = [];
// 参数
this._args = args;
// 监听器,或者是订阅者
this.taps = [];
// 拦截器
this.interceptors = [];
// 这几个函数最终其实指向的是 Object 的原型
// 普通调用
this.call = this._call;
// promise 调用
this.promise = this._promise;
// 异步调用
this.callAsync = this._callAsync;
// 全部的回调函数
this._x = undefined;
}
复制代码
经过 Hook 收集的信息,而后经过 HookCodeFactory 来生成函数,好比上面的 SyncHook 代码,最终生成的代码以下:
"use strict";
var _context;
// _x 保存了全部的回调函数,也就是 tap 时的函数
// syncHook.tap('SuyanSyncHook', FN);
var _x = this._x;
// 执行第一个回调函数
var _fn0 = _x[0];
_fn0(name, age);
// 执行第二个回调函数
var _fn1 = _x[1];
_fn1(name, age);
复制代码
tapable 的核心就是动态生成函数 Function。在 JavaScript 中能够直接定义函数,也能够经过 new Function 来生成一个函数,下面的函数是等价的:
// 直接定义函数
function sum(a, b) {
return a + b;
}
// 经过 new Function 生成函数
const sum = new Function(['a', 'b'], 'return a + b;');
复制代码
这就是 Hook 的本质,能够经过最终生成的代码理解每个 Hook 的做用。
总之,tapable 的核心原理是收集函数要执行时的所有信息,根据这些信息 taps、intercepts、args、type ,经过 new Function 生成最终要调用的函数,当须要通知监听者时,直接执行生成的函数。
能够通 Hook 的名字理解它做用,好比 SyncHook,它很「纯」,最基本的 Hook。而 SyncBailHook、SyncWaterfallHook、SyncLoopHook 这 3 个 Hook 添加了修饰符,它们与监听函数的返回值有关。
Bail(熔断),带有这个词的 Hook 表示只要有一个 Hook 的监听函数返回不为 undefined,监听就会截止。因为第二个监听函数返回了「学习 webpack」,第三个监听函数将不会被执行。
const SyncBailHook = require('./lib/SyncBailHook');
// 一个接一个的 hook,只要有一个返回 undefined 的就截止
const syncBailHook = new SyncBailHook(['source']);
syncBailHook.tap('SuyansyncBailHook', (source) => {
console.log(`source1: ${source}`);
});
syncBailHook.tap('SuyansyncBailHook', source => {
console.log(`source2: ${source}`);
return '学习 webpack';
});
syncBailHook.tap('SuyansyncBailHook', source => {
console.log(`source3: ${source}`);
});
let ret = syncBailHook.call('关注公众号素燕,');
console.log(`ret = ${ret}`);
复制代码
咱们在看下编译后的代码:
"use strict";
var _context;
var _x = this._x;
console.log('this._x = : ', this._x);
var _fn0 = _x[0];
var _result0 = _fn0(source);
if (_result0 !== undefined) {
return _result0;
} else {
var _fn1 = _x[1];
var _result1 = _fn1(source);
if (_result1 !== undefined) {
return _result1;
} else {
var _fn2 = _x[2];
var _result2 = _fn2(source);
if (_result2 !== undefined) {
return _result2;
} else {
}
}
}
复制代码
waterfall(瀑布流),监听函数的返回值会传递给下一个监听函数,就如同水流同样,源源不断。每个监听函数的返回值会传递到下一个回调函数,下面这个 demo 最终的返回值为「关注公众号素燕,和素燕一块儿学习 webpack」:
const SyncWaterfallHook = require('./lib/SyncWaterfallHook');
// 一个接一个的 hook,上一个函数的返回值是下一个函数的参数
const syncWaterfallHook = new SyncWaterfallHook(['source']);
syncWaterfallHook.tap('SuyanSyncWaterfallHook', (source) => {
console.log(`source1: ${source}`);
return `${source}和`;
});
syncWaterfallHook.tap('SuyanSyncWaterfallHook', source => {
console.log(`source2: ${source}`);
return `${source}素燕`;;
});
syncWaterfallHook.tap('SuyanSyncWaterfallHook', source => {
console.log(`source3: ${source}`);
return `${source}一块儿学习 webpack`;;
});
let ret = syncWaterfallHook.call('关注公众号素燕,');
console.log(`ret = ${ret}`);
复制代码
最终编译的代码以下:
"use strict";
var _context;
var _x = this._x;
var _fn0 = _x[0];
var _result0 = _fn0(source);
if(_result0 !== undefined) {
source = _result0;
}
var _fn1 = _x[1];
var _result1 = _fn1(source);
if(_result1 !== undefined) {
source = _result1;
}
var _fn2 = _x[2];
var _result2 = _fn2(source);
if(_result2 !== undefined) {
source = _result2;
}
return source;
复制代码
loop(循环),只要返回值不为「假值」,监听函数将一直被调用。不详细讲解,可自行学习。
第二类是异步的 Hook,这种 Hook 通常用在比较耗时的操做,好比网络请求。
对于异步 hook,可使用 tapAsync 和 tapPromise 添加异步函数。监听函数的最后一个参数为 callback,调用 callback 时,若是带有 error 参数,整个监听函数链将会中断,好比 callback('err'),当遇到这种 callback 时,下一个监听函数将不会被执行。举个例子:
const AsyncSeriesHook = require('./lib/AsyncSeriesHook');
// 异步串行,就是一个一个来
const asyncSeriesHook = new AsyncSeriesHook(['source']);
asyncSeriesHook.tapAsync('SuyanasyncSeriesHook', (source, callback) => {
setTimeout(() => {
console.log(`source1: ${source}`);
callback();
}, 3000);
});
asyncSeriesHook.tapAsync('SuyanasyncSeriesHook', (source, callback) => {
setTimeout(() => {
console.log(`source2: ${source}`);
callback();
}, 1000);
});
asyncSeriesHook.callAsync('关注公众号素燕', ret => {
console.log(`ret = ${ret}`);
});
复制代码
经过最终编译后的代码能够清晰看到整个 hook 作的事情:
"use strict";
var _context;
var _x = this._x;
// 下一个要执行的函数
function _next0() {
var _fn1 = _x[1];
_fn1(source, _err1 => {
if (_err1) {
_callback(_err1);
} else {
_callback();
}
});
}
// 第一个要执行的函数
var _fn0 = _x[0];
_fn0(source, _err0 => {
if (_err0) {
_callback(_err0);
} else {
_next0();
}
});
复制代码
这个 Hook 和 SyncBailHook 大同小异,只不过回调函数为异步函数,回调函数 callback 接收 2 个参数,第一个为 error,第二个为 result,当遇到 error 和 result 都不为空时,整个监听函数链将会中断。看下面的例子:
const AsyncSeriesBailHook = require('./lib/AsyncSeriesBailHook');
// 异步串行,就是一个一个来
const asyncSeriesBailHook = new AsyncSeriesBailHook(['source']);
asyncSeriesBailHook.tapAsync('SuyanasyncSeriesBailHook', (source, callback) => {
setTimeout(() => {
console.log(`source1: ${source}`);
callback();
}, 3000);
});
asyncSeriesBailHook.tapAsync('SuyanasyncSeriesBailHook', (source, callback) => {
setTimeout(() => {
console.log(`source2: ${source}`);
callback();
}, 1000);
});
asyncSeriesBailHook.callAsync('关注公众号素燕', ret => {
console.log(`ret = ${ret}`);
});
复制代码
最终编译的代码以下,与 AsyncSeriesHook 很类似,只是在 callback 中多了一个 result 参数:
"use strict";
var _context;
var _x = this._x;
console.log('this._x = : ', this._x);
function _next0() {
var _fn1 = _x[1];
_fn1(source, (_err1, _result1) => {
if (_err1) {
_callback(_err1);
} else {
if (_result1 !== undefined) {
_callback(null, _result1);
;
} else {
_callback();
}
}
});
}
var _fn0 = _x[0];
_fn0(source, (_err0, _result0) => {
if (_err0) {
_callback(_err0);
} else {
if (_result0 !== undefined) {
_callback(null, _result0);
;
} else {
_next0();
}
}
});
复制代码
这个和 SyncWaterfallHook 相似,上一个回调结果会传递到下一个监听函数中。
异步循环 Hook,只要回调函数不返回 undefined,循环将一直持续执行,知道回调函数的值为 undefined。
异步并行 hook,也就是说全部的回调函数同时进行。
异步并行熔断钩子。
tapable 对于初学者来讲并不友好,学习起来有必定的学习成本。总的来讲,tapable 主要分红同步 hook 和异步 hook,每类 hook 下又分为了 bail(当函数有任何返回值时,接下来的 hook 回调函数将终止)、waterfall(瀑布流,函数返回值将向下一个回调函数传递)、loop(循环,只要函数返回值不为 undefined,将一直循环)。若是想完全理解各类类型的 hook,能够经过分析最终生成的函数代码来理解,理解起来很是容易。
这节内容属于理论知识,下一节内容咱们结合 compiler 来加深对 tapable 的理解。