【重学webpack系列——webpack5.0】
1-15节主要讲webpack的使用,当然,建议结合《webpack学完这些就够了》一起学习。
从本节开始,专攻webpack原理,只有深入原理,才能学到webpack设计的精髓,从而将技术点运用到实际项目中。
可以点击上方专栏订阅哦。
以下是本节正文:
- webpack实现插件的流程:
- 创建 - webpack在其内部对象上创建各种钩子Hook
- 注册 - 插件讲自己的方法注册到对应的钩子Hook上,交给webpack
- 调用 - webpack编译过程中,会适时地触发响应钩子Hook,因此也就触发了插件的方法。
- webpack本质是一种事件流的机制,它的工作流程就是将各个插件串联起来,实现这个的核心是
Tapable
,webpack中最核心的负责编译
的Compiler
和负责创建bundle
的Compliation
都是Tapable
的实例。
1.hook的分类-按运行顺序分
根据hook的类型可以分为
同步Sync和异步Async
,异步又可以分为并行
和串行
- Tapable
- 同步Hook
- SyncHook
- SyncBailHook
- SyncWaterfallHook
- SyncLoopHook
- 异步钩子
- 异步并行
- AsyncParallelHook
- AsyncParallelBailHook
- 异步串行
- AsyncSeriesHook
- AsyncSeriesBailHook
- AsyncSeriesWaterHook
- 异步并行
- 同步Hook
2.hook的分类-按结果分
根据返回的类型可以分为Basic、Bail、Waterfall、Loop
- Tapable
- Basic 按顺序执行每个事件函数,不关系函数的返回值
- SyncHook
- AsyncParallelHook
- AsyncSeriesHook
- Bail 执行每个事件函数,遇到结果不为undefined则结束执行,直接走call的回调
- SyncBailHook
- AsyncParallelBailHook
- AsyncSeriesBailHook
- Waterfall 将最近上一个函数的返回值作为下一个函数的参数,如果上一个函数没有返回值(返回undefined),那么就继续找上上个,如果找不到就用自己传入的参数
- SyncWaterfallHook
- AsyncSeriesWaterfallHook
- Loop 不停的循环执行事件函数,直到所有函数结果 result === undefined,每次循环都是从头开始的
- SyncLoopHook
- Basic 按顺序执行每个事件函数,不关系函数的返回值
3.九大Hook使用
1.SyncHook
const { SyncHook } = require('tapable');
const syncHook = new SyncHook(['name', 'age222']); // ['name', 'age']代表形参。其实这两的参数有用的是参数的长度,代表取call的参数的个数,至于元素是什么 没什么关系
// 注册事件函数
syncHook.tap('1', (name, age) => {
console.log(1, name, age);
})
syncHook.tap('2', (name, age) => {
setTimeout(() => {
console.log(2, name, age);
})
return 1
})
syncHook.tap('3', (name, age) => {
console.log(3, name, age);
})
// 触发事件函数
syncHook.call('yh', 18);
2.SyncBailHook
const { SyncBailHook } = require('tapable')
const syncBailHook = new SyncBailHook(['name', 'age']);
syncBailHook.tap('1', (name, age) => {
console.log(1, name, age)
})
syncBailHook.tap('2', (name, age) => {
console.log(2, name, age)
return '2'
})
syncBailHook.tap('3', (name, age) => {
console.log(3, name, age)
})
syncBailHook.call('yh', 18)
3.SyncWaterfallHook
const { SyncWaterfallHook } = require('./myHook/3.SyncWaterfailHook');
const syncWaterfallHook = new SyncWaterfallHook(['name', 'age']);
syncWaterfallHook.tap('1', (name, age) => {
console.log(1, name, age)
return '名字1'
})
syncWaterfallHook.tap('2', (name, age) => {
console.log(2, name, age)
return '名字2'
})
syncWaterfallHook.tap('3', (name, age) => {
console.log(3, name, age)
return '名字3'
})
syncWaterfallHook.tap('4', (name, age) => {
console.log(4, name, age)
})
syncWaterfallHook.call('yh', 18)
// waterfallHook将最近上一个函数的返回值作为下一个函数的参数,如果上一个函数没有返回值(返回undefined),那么就继续找上上个,如果找不到就用自己传入的参数
4.SyncLoopHook
const { SyncLoopHook } = require('./myHook/4.SyncLoopHook');
// 不停的循环执行事件函数,直到所有函数结果 result === undefined
// 每次循环都是从头开始的
const syncLoopHook = new SyncLoopHook(['name', 'age']);
let counter1 = 0;
let counter2 = 0;
let counter3 = 0;
syncLoopHook.tap('1', (name, age) => {
console.log(1, 'counter1', counter1, name, age);
if (++counter1 === 1) {
counter1 = 0;
return;
}
return true;
})
syncLoopHook.tap('2', (name, age) => {
console.log(2, 'counter2', counter2, name, age);
if (++counter2 === 2) {
counter2 = 0;
return;
}
return true;
})
// syncLoopHook.tap('3', (name, age) => {
// console.log(3, 'counter3', counter3, name, age);
// if (++counter3 === 3) {
// counter3 = 0;
// return;
// }
// return true;
// })
syncLoopHook.call('yh', 18);
5.AsyncParallelHook
该钩子有三种注册监听事件的方式:
-
tap 注册同步钩子,按顺序执行回调,不需要等待队列都处理完才出发callAsync回调。
- callAsync 触发监听事件
-
tapAsync 注册异步果子,全部任务完成后执行最终回调
- callAsync 触发监听事件
-
tapPromise 用promise注册异步钩子,全部完成后才算成功
- promise 触发监听事件
const { AsyncParallelHook } = require('tapable')
let queue = new AsyncParallelHook(["name"]);
// 同步注册钩子,按顺序执行回调,不需要等待队列都处理完才出发callAsync回调。
// 下面的执行答案为:1、2、undefined 错误信息、3
queue.tap("1", function (name) {
console.log(1);
});
queue.tap("2", function (name) {
console.log(2);
});
queue.tap("3", function (name) {
setTimeout(() => {
console.log(3);
}, 3000)
});
queue.callAsync("yh", (err) => {
console.log(err, '错误信息');
});
// 异步注册钩子,需要全部任务完成后才执行callAsync回调
// callback一旦传入参数,就会走触发监听事件的函数,也就是callAsync
// 且一旦走过触发监听事件的函数callAsync,就不会再走了。比如有两个callback有入参,那只会是第一个callback执行的时候去执行callAsync,第二个callback不会触发callAsync了。
queue.tapAsync('1', (name, callback) => {
setTimeout(() => {
console.log(1);
callback(111);
}, 1000)
})
queue.tapAsync('2', (name, callback) => {
setTimeout(() => {
console.log(2);
callback(222);
}, 2000)
})
queue.tapAsync('3', (name, callback) => {
setTimeout(() => {
console.log(3);
callback();
}, 3000)
})
queue.callAsync('yh', err => {
console.log(err, '错误信息');
})
// tapPromise注册钩子,注册时,必须返回一个promise
// resolve()的参数是没有作用的,reject的参数会传到queue.promise的错误回调中。且一旦触发reject,就会执行promise的错误回调,并不会等到玩不执行完后去执行。当然,执行了reject,也不会阻止事件的继续触发。
queue.tapPromise('1', (name) => {
return new Promise((resolve, reject) => {
setTimeout(function () {
console.log(1);
resolve(111);
}, 1000);
})
})
queue.tapPromise('1', (name) => {
return new Promise((resolve, reject) => {
setTimeout(function () {
console.log(2);
reject(222);
}, 2000);
})
})
queue.tapPromise('1', (name) => {
return new Promise((resolve, reject) => {
setTimeout(function () {
console.log(3);
resolve();
}, 3000);
})
})
queue.promise('yh').then((res) => {
console.log('成功触发', res)
}, (err) => {
console.log('错误触发', err)
})
6.AsyncParallelBailHook
AsyncParallelBailHook
-
带保险的异步并执行钩子
-
有一个任务返回值不为undefined就结束
-
对于promise来说,resolve和reject并没有区别
-
区别在于是否给他们传了参数
该钩子有三种注册监听事件的方式:
-
tap 注册同步钩子,按顺序执行回调,不需要等待队列都处理完才出发callAsync回调,因为是bailHook,所以有返回值则结束,直接走触发函数callAsync回调
- callAsync 触发监听事件
-
tapAsync 注册异步果子,全部任务完成后执行最终回调
- callAsync 触发监听事件
-
tapPromise 用promise注册异步钩子,只要有一个任务有 resolve 或者 reject 值,不管成功失败都结束
- promise 触发监听事件
let { AsyncParallelBailHook } = require("tapable");
let queue = new AsyncParallelBailHook(["name"]);
// tap同步注册,callAsync触发,不需要等待队列都处理完才出发callAsync回调,因为是bailHook,所以有返回值则结束,直接走触发函数callAsync回调
queue.tap("1", function (name) {
console.log(1);
});
queue.tap("2", function (name) {
console.log(2);
return 'aaa';
});
queue.tap("3", function (name) {
setTimeout(() => {
console.log(3)
}, 3000);
});
queue.callAsync("yh", (err) => {
console.log(err, '错误信息');
});
// tapAsync注册,callAsync触发,任务全部完成才触发CallAsync回调,因为是bailHook,所以有返回值则结束,直接走触发函数callAsync回调
queue.tapAsync("1", function (name, callback) {
console.log(1);
callback("Wrong");
});
queue.tapAsync("2", function (name, callback) {
console.log(2);
callback();
});
queue.tapAsync("3", function (name, callback) {
console.log(3);
callback();
});
queue.callAsync("yh", (err) => {
console.log(err);
});
// tapPromise, 只要有一个任务有 resolve 或者 reject 值,不管成功失败都结束,结束的意思是都走callAsync触发函数的回调,并不是说其他注册的函数都不执行了,执行还是要执行的。
// resolve或reject执行或一次后面的就无效了,无效的意思是不会再出发callAsync函数了
queue.tapPromise("1", function (name) {
return new Promise(function (resolve, reject) {
setTimeout(function () {
console.log(1);
//对于promise来说,resolve还reject并没有区别
//区别在于你是否传给它们的参数
resolve(1);
}, 1000);
});
});
queue.tapPromise("2", function (name) {
return new Promise(function (resolve, reject) {
setTimeout(function () {
console.log(2);
reject(2); // resolve或reject执行或一次后面的就无效了。
}, 2000);
});
});
queue.tapPromise("3", function (name) {
return new Promise(function (resolve, reject) {
setTimeout(function () {
console.log(3);
resolve();
}, 3000);
});
});
queue.promise("yh").then(
(result) => {
console.log("成功", result);
},
(err) => {
console.error("失败", err);
}
);
7.AsyncSeriesHook
AsyncSeriesHook
-
异步穿行钩子
-
任务一个一个执行,执行完上一个执行下一个
// tap同步注册,callAsync触发,不需要等待队列都处理完才出发callAsync回调,
queue.tap("1", function (name) {
console.log(1);
});
queue.tap("2", function (name) {
console.log(2);
});
queue.tap("3", function (name) {
setTimeout(() => {
console.log(3);
})
});
queue.callAsync("yh", (err) => {
console.log(err);
});
// tapAsync异步注册钩子,一个执行完了才会执行下一个,前提是调用callback(),如果没有callback(),那么就结束掉了,连callAsync都不会触发
queue.tapAsync("1", function (name, callback) {
setTimeout(function () {
console.log(1);
callback();
}, 1000);
});
queue.tapAsync("2", function (name, callback) {
setTimeout(function () {
console.log(2);
}, 2000);
});
queue.tapAsync("3", function (name, callback) {
setTimeout(function () {
console.log(3);
callback();
}, 3000);
});
queue.callAsync("yh", (err) => {
console.log(err, '错误信息');
});
// tapPromise注册监听,需要返回一个promise,一个promise执行完才执行下一个。resolve的参数是无效的,resolve也不能阻止允许,但是reject调用会阻止允许,直接进入queue.promise的失败回调,参数有效。
queue.tapPromise("1", function (name) {
return new Promise(function (resolve) {
setTimeout(function () {
console.log(1, name);
resolve();
}, 1000);
});
});
queue.tapPromise("2", function (name) {
return new Promise(function (resolve, reject) {
setTimeout(function () {
console.log(2, name);
reject('e');
}, 2000);
});
});
queue.tapPromise("3", function (name) {
return new Promise(function (resolve) {
setTimeout(function () {
console.log(3, name);
resolve();
}, 3000);
});
});
queue.promise("yh").then((data) => {
console.log(data);
}, (err) => {
console.log(err);
});
8.AsyncSeriesBailHook
AsyncSeriesBailHook
- 只要有一个返回了不为 undefined 的值就直接结束
let { AsyncSeriesBailHook } = require("tapable");
let queue = new AsyncSeriesBailHook(["name"]);
// tap
queue.tap("1", function (name) {
console.log(1);
return "Wrong";
});
queue.tap("2", function (name) {
console.log(2);
});
queue.tap("3", function (name) {
console.log(3);
});
queue.callAsync("yh", (err) => {
console.log(err);
});
//tapAsync
queue.tapAsync("1", function (name, callback) {
setTimeout(function () {
console.log(1);
callback("wrong");
}, 1000);
});
queue.tapAsync("2", function (name, callback) {
setTimeout(function () {
console.log(2);
callback();
}, 2000);
});
queue.tapAsync("3", function (name, callback) {
setTimeout(function () {
console.log(3);
callback();
}, 3000);
});
queue.callAsync("yh", (err) => {
console.log(err);
});
// tapPromise
queue.tapPromise("1", function (name) {
return new Promise(function (resolve) {
setTimeout(function () {
console.log(1);
resolve();
}, 1000);
});
});
queue.tapPromise("2", function (name, callback) {
return new Promise(function (resolve, reject) {
setTimeout(function () {
console.log(2);
reject("失败了");
}, 2000);
});
});
queue.tapPromise("3", function (name, callback) {
return new Promise(function (resolve) {
setTimeout(function () {
console.log(3);
resolve();
}, 3000);
});
});
queue.promise("yh").then(
(data) => {
console.log(data);
console.timeEnd("cost");
},
(error) => {
console.log(error);
}
);
9.AsyncSeriesWaterfallHook
AsyncSeriesWaterfallHook
- 只要有一个返回了不为 undefined 的值就直接结束
let { AsyncSeriesWaterfallHook } = require("tapable");
let queue = new AsyncSeriesWaterfallHook(["name", "age"]);
// tap
queue.tap("1", function (name, age) {
console.log(1, name, age);
return "return1";
});
queue.tap("2", function (data, age) {
console.log(2, data, age);
return "return2";
});
queue.tap("3", function (data, age) {
console.log(3, data, age);
});
queue.callAsync("yh", 10, (err) => {
console.log(err);
});
//tapAsync
queue.tapAsync("1", function (name, age, callback) {
setTimeout(function () {
console.log(1, name, age);
callback(null, 1);
}, 1000);
});
queue.tapAsync("2", function (data, age, callback) {
setTimeout(function () {
console.log(2, data, age);
callback(null, 2);
}, 2000);
});
queue.tapAsync("3", function (data, age, callback) {
setTimeout(function () {
console.log(3, data, age);
callback(null, 3);
}, 3000);
});
queue.callAsync("yh", 10, (err, data) => {
console.log(err, data);
});
// tapPromise
queue.tapPromise("1", function (name) {
return new Promise(function (resolve) {
setTimeout(function () {
console.log(name, 1);
resolve(1);
}, 1000);
});
});
queue.tapPromise("2", function (data) {
return new Promise(function (resolve) {
setTimeout(function () {
console.log(data, 2);
resolve(2);
}, 2000);
});
});
queue.tapPromise("3", function (data) {
return new Promise(function (resolve) {
setTimeout(function () {
console.log(data, 3);
resolve(3);
}, 3000);
});
});
queue.promise("yh").then((err) => {
console.log("err", err);
});
4.实现SyncHook
- tapable目录结构
-----myTapable
|_____index.js
|_____Hook.js
|_____SyncHook.js
|_____HookCodeFactory.js
-
实现:
-
index.js
// index.js const SyncHook = require('./SyncHook.js'); module.exports = { SyncHook }
-
Hook.js
/* 懒编译: 这个CALL_DELEGATE这样设计比较精妙,因为会将this.call重新赋值,这种就属于懒编译,有两个特点: 1. 我调用的时候编译,不调用不编译 2. 调用一次,永久使用。因为调用过一次后,this.call就被重新赋值了,那么下次调this.call的时候,就不需要再走CALL_DELEGATE了,直接走重新赋值的那个内容了 懒编译举个使用场景: 答:根据浏览器类型来执行某个逻辑的时候,比如: let doSomething = (browser) => { if ('IE') { doSomething = () => {...} }else if ('Chrome') { doSomething = () => {...} }else if ('FireFox') { doSomething = () => {...} } } doSomething('Chrome'); doSomething(); doSomething(); */ const CALL_DELEGATE = function(...args){ // 先动态创建一个sync同步的类型的call方法,然后赋值给this.call // 关于this的问题,只有一句话记住就可以了——谁调用它就指向谁。这里指向钩子的实例,也就是syncHook.call(),所以this指向syncHook this.call = this._createCall('sync'); return this.call(...args); } class Hook{ constructor(args = []){ this.args = args; //['name', 'age'] this.taps = []; // 存放注册的事件,也就是订阅的函数放在这个数组中,里面放了[{type, name, fn}], 注意下面的this._x this.call = CALL_DELEGATE; this._x = undefined; // 这其实才是真正存放我们时间函数的数组,里面放了[fn],注意上面的this.taps。this.taps.map(tap => tap.fn) => this._x } tap(options, fn){ this._tap('sync', options, fn) } _tap(type, options, fn){ if (typeof options === 'string') { options = {name: options}; } // 创建tapInfo,并且插入到this.taps数组中去 let tapInfo = {...options, type, fn}; this._insert(tapInfo); } _resetCompilation(){ this.call = CALL_DELEGATE; } _insert(tapInfo){ this._resetCompilation(); // 每次插入新的函数之前,需要重新编译call方法。因为编译过一次之后CALL_DELEGATE中的_createCall会动态创建一个函数,这个函数编译了x个tap函数,这时候新增了一个,那就是x+1个函数了,不再使用原来的x个的了,所以需要重新编译出x+1个tap的函数出来,所以需要重写 this.taps.push(tapInfo); } compile(options){ throw new Error('Abstract:此方法应该被子类重写') } _createCall(type){ // 动态创建一个函数 return this.compile({ taps: this.taps, // 要执行的函数 args: this.args, // 事件函数接收的参数 type // 执行的类型 sync }) } } module.exports = Hook;
-
HookCodeFactory
class HookCodeFactory{ setup(hookInstance, options){ // 把tapInfo中的fn取出来变成数组赋值给hookInstance._x hookInstance._x = options.taps.map(tapInfo => tapInfo.fn); } init(options){ this.options = options; } deInit(){ this.options = null; } args(options){ let allArgs = options.args || []; // ['name', 'age'] return allArgs.join(','); // 'name,age' } header(){ // new Function()的函数体内头和内容两部分,头是固定的,为`var _x = this._x;`,内容根据实际来定 let code = ``; code += `var _x = this._x;\n`; return code; } callTapsSeries(options){ if (options.taps.length === 0) { return ''; // 如果没有注册过任何的事件函数,内容就是空的 } let code = ``; for(let i = 0; i < options.taps.length; i++) { let content = this.callTap(i, options); code += content; } return code; } callTap(tapIndex, options) { // 这里就是构造函数体的内容部分 // 这个方法要返回var _fn0 = _x[0];_fn0(name, age); let code = ``; code += `var _fn${tapIndex} = _x[${tapIndex}];\n` let tapInfo = options.taps[tapIndex]; //{name, fn, type} switch(tapInfo.type) { case 'sync': code += `_fn${tapIndex}(${this.args(options)});\n`; break; default: break; } this.deInit(); return code } create(options){ this.init(options); let fn; switch(this.options.type) { case 'sync': fn = new Function( this.args(options),// 'name,age' this.header() + this.content(options), //函数体 ); break; default: break; } return fn; } } module.exports = HookCodeFactory;
-
SyncHook.js
const Hook = require('./Hook') const HookCodeFactory = require('./HookCodeFactory'); class SyncHookCodeFactory extends HookCodeFactory{ content(options){ // 穿行调用taps函数,也就是拿出fn0执行,拿出fn1执行,拿出fn2执行... return this.callTapsSeries(options); } } let factory = new SyncHookCodeFactory(); class SyncHook extends Hook{ compile(options) { // 把钩子的实例和选项的值用来初始化代码工厂 factory.setup(this, options); // options = {type: 'sync', taps, args} // 根据选项创建call方法,实现:new Function(args, 函数里(里面会调用taps)) return factory.create(options); } } module.exports = SyncHook;
-
总结:
实现一个SyncHook,主要是实现Hook类,这个Hook提供了tap注册事件的方法,其实就是将时间放到一个队列中,然后通过call方法去触发事件队列。如果我们自己写,其实可以直接遍历这个队列,然后一个个循环执行即可。在tapable中,其实是通过compile方法动态创建一个函数得到的,这个complier是通过集成HookCodeFactory类得到的。那么怎么动态创建,其实就是new Function(),然后第一个参数是函数参数组成的字符串,第二个参数是函数体字符串,那么函数体其实是有两部分组成的,第一部分是固定的,内容为
var _x = this._x;\n
,第二部分的内容字符串,其实就是每个函数的调用,并传入参数,构成的字符串。这样new出来的Function去执行即可。最后最好简单的一句话概括下:本质就是将触发的函数放到一个数组中,然后一个个触发,这也就是订阅发布的模式。
**
-
问:为什么compiler是动态new一个Function,而不是直接去遍历taps?
因为这个compiler是统一的编译方法,适合九大Hook,每个Hook的对taps的函数执行的逻辑都不一样,比较灵活和复杂,所以不再为每个hook为写一套遍历taps的逻辑,而是统一写一个new Function,传入参数字符串和函数体字符串即可。
5.实现AsyncParallelHook异步并行——tapAsync
原理:这个跟上面的区别在于compiler函数动态new一个Function的函数体不同,函数体如下图所示,即:等到_counter为0的时候调用回调函数,这个原理跟promise.all差不多。
6.实现AsyncParallelHook异步并行——promise
原理:这个跟上面的区别在于compiler函数动态new一个Function的函数体不同,函数体如下图所示,即:等到_counter为0的时候调用回调函数,这个原理跟promise.all差不多。
7.实现AsyncSeriesHook异步串行——tapAsync
原理:这个跟上面的区别在于compiler函数动态new一个Function的函数体不同,函数体如下图所示,其实就是从callAsync开始一层套一层
8.hook的拦截器interceptor
8.1 拦截器的三大API
tapable的每个hook都提供了拦截器interceptor,该拦截器主要提供常用的三大API为:
- register
- 当注册一个新的事件函数的视乎,会触发register函数
- tap
- 每个事件函数执行的时候(不是注册的时候,是触发的时候),会触发tap函数
- call
- 每一个call的触发会触发一次该拦截器
8.2拦截器的使用
首先,父类Hook类提供了intercept方法,参数为interceptor,执行该方法就相当于注册了一个拦截器。
另外,每个hook可以注册多个拦截器,比如下面就数注册了两个拦截器:
const { SyncHook } = require('./myTapable/index');
const syncHook = new SyncHook(['name', 'age']);
let interceptor1 = {
// 当你注册一个新的事件函数的额时候,会触发register
register(tapInfo){
console.log('拦截器1开始register', tapInfo); // tapInfo的内容就是当前注册函数需要保存在taps队列中的信息,有type,fn和name三个属性,比如:{ type: 'sync', fn: [Function], name: '事件函数1' }
tapInfo.flag = 'uuu'; // 做个标志,看看interceptor2的register的参数tapInfo会不会有flag
return tapInfo;
},
// tap拦截器,是在每个事件函数触发的时候(不是注册的时候,是触发的时候)执行,准确的说是每个事件触发之前
tap(tapInfo){
console.log('拦截器1开始tap', tapInfo);
},
call(name, age){ // 每一个call会触发一次call拦截器,准确得说是每个call触发之前
console.log('拦截器1开始call', name, age);
}
};
let interceptor2 = {
register(tapInfo){ // 这里的tapInfo,是来自于上一个interceptor的register的,所以这个tapInfo会有上面的flag标志
console.log('拦截器2开始register', tapInfo);
return tapInfo;
},
tap(tapInfo){
console.log('拦截器2开始tap', tapInfo);
},
call(name, age){ // 每一个call会触发一次call拦截器,准确得说是每个call触发之前
console.log('拦截器2开始call', name, age);
}
};
syncHook.intercept(interceptor1)
syncHook.intercept(interceptor2) // interceptor可以多个,那么每次注册新的事件,也就是tap的时候,会将所有interceptor的register执行一遍
源码中,会把每个拦截器放在一个数组中,每次触发拦截器API的时候,会遍历该数组。所以,如上面的例子,注册一个时间函数,会触发两次register,触发一个事件,会触发两次tap,触发一次call,会触发两次拦截器的call
8.3拦截器API的参数
拦截器API的参数(默认有一个),
- 一般为当前注册函数需要保存在taps队列中的信息,有type,fn和name三个属性,比如:{ type: ‘sync’, fn: [Function], name: ‘事件函数1’ }
- 如果一个拦截器的钩子有返回值,会进入下一个拦截器同类钩子的第一个参数
9.HookMap
HookMap的实例是map,可以用map来批量创建钩子
- 举例:
let { SyncHook, HookMap } = require('tapable');
const map = new HookMap(() => new SyncHook(['name']));
// map是用来批量创建钩子的 key是字符串 值是一个钩子hook,就是上面第二行new HookMap()的参数执行的结果。
map.for('key1').tap('plugin1', (name) => console.log(1, name));
map.for('key1').tap('plugin2', (name) => console.log(2, name));
map.get('key1').call('yh');
map.for('key2').tap('plugin3', (age) => console.log(3, age));
map.for('key2').tap('plugin3', (age) => console.log(4, age));
map.get('key2').call(18);
- map是用来批量创建钩子的 key是字符串 值是一个钩子hook,就是上面第二行new HookMap()的参数执行的结果。
9.1HookMap的实现
class HookMap {
constructor(factory) {
this._map = new Map();
this._factory = factory;
}
get(key) {
return this._map.get(key);
}
tapAsync(key, options, fn) {
return this.for(key).tapAsync(options, fn);
}
tapPromise(key, options, fn) {
return this.for(key).tapPromise(options, fn);
}
for(key) {
const hook = this.get(key);
if (hook) return hook;
let newHook = this._factory();
this._map.set(key, newHook);
return newHook;
}
}
module.exports = HookMap;
10.stage
每个钩子在注册事件函数的时候,也就是调用tap的时候,第一个参数有以下几种情况:
- 可以是字符串,比如字符串’xxx’,那么源码会转成{name: ‘xxx’}
- 可以是对象{name: ‘xxx’},如果是对象,还可以加一个stage属性,值为数字类型,比如{name: ‘xxx’, stage: 2}
这个stage的大小,代表该tap回调触发的顺序,越小的越先触发。
- 使用
let {SyncHook} = require('tapable');
let hook = new SyncHook(['name']);
hook.tap({name:'tap1',stage:1},(name)=>{
console.log(1,name);
});
hook.tap({name:'tap3',stage:3},(name)=>{
console.log(3,name);
});
hook.tap({name:'tap5',stage:5},(name)=>{
console.log(4,name);
});
hook.tap({name:'tap2',stage:2},(name)=>{
console.log(2,name);
});
hook.call('yh'); // 按照stage的值大小为先后顺序来执行
-
实现:一个插入排序,即:每注册一个事件函数的时候,都会根据stage的大小,在已有的taps队列中进行插入。
let stage = 0; if (typeof tapInfo.stage === "number") { stage = tapInfo.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 (xStage > stage) { continue; } i++; break; } this.taps[i] = tapInfo;
11.before
指定tap在哪些tap之前执行
- 使用:
let {SyncHook} = require('tapable');
let hook = new SyncHook(['name']);
hook.tap({name:'tap1'},(name)=>{
console.log(1,name);
});
hook.tap({name:'tap3'},(name)=>{
console.log(3,name);
});
hook.tap({name:'tap5'},(name)=>{
console.log(4,name);
});
hook.tap({name:'tap2',before:['tap3','tap5']},(name)=>{
console.log(2,name);
});
hook.call('yh');