webpack系列文章
写在前面
在之前的文章中,我们进行了webpack
的源码粗劣阅读,详见webpack源码阅读一:webpack流程以及重要钩子。在这篇文章中,我们只是粗劣地列举出了webpack的各个阶段,以及每个阶段触发的各个钩子。我们并没有深入地去查看每个钩子,这是因为webpack
的钩子应用了一个名叫tapable
的库,这个库提供了钩子的各种功能,因此在深入查看webpack
的各个钩子之前,我们必须先了解tapable
这个库。而这会给我们阅读源码带来障碍,因为我们需要分心去学一个新的东西,这影响了我们上一篇读源码的初始目的,只是去了解webpack的各个阶段。而我们想要进一步去熟悉webpack的源码,那么就绕不开tapable
,因此,本篇文章我将会带大家去认识tapable
这个东西。同理,正如我在之前的文章中所说,掌握一个东西,最好的办法就是去实现一个简易的它,因此本文我们会实现一个简易的tapable
库,将webpack中使用到的tapable
核心类都实现一遍。我始终认为只有自己动手做过的东西才会印象最深刻。
tapable是什么?
看过webpack
源码的同学可能都会发现,在源码中突然会出现一些compiler.hooks.done.tap(xxx,callback)
这种代码,也就是突然触发一个钩子,我们也不知道这个钩子在哪里定义的。实际上这就是类似于我们常说的发布订阅模式,都是注册一个事件,然后到了适当的时候。以我们最常见的node.js的Event机制为例。通过on
方法注册一个事件,然后通过emit
方法进行触发。
const EventEmitter = require("event");
const myEmitter = new EventEmitter();
myEmitter.on("js",(..args) => {
console.log(...args);
})
myEmitter.emit("js","前端课程");
tapable的机制与Event类似,它可以用来定义各种各样的钩子,相当于Event中的事件注册,但是与Event不同的是Event中各个事件之间相互不关联,互不影响,但是tapable注册的事件之间可以是有关系的,这种关系通过tapable
定义的各个钩子类来实现。其实,tapable
的核心就是这些钩子类。因此,我们接下来的重点就是讲述这些钩子类,并实现它们。
tapable的各个钩子
如上图所示,列举出了tapable的所有的钩子。
根据同步和异步分为:
- Sync*:同步钩子。所有钩子同步执行。
- Async*:异步钩子。钩子异步执行。
每种钩子根据与其他钩子之间的关系,又可以分为:
- 普通型钩子Hook:就是各个钩子之间没有关联,大家按照注册顺序依次执行,互不影响。
- 熔断型钩子BallHook:就是如果上一个钩子返回出undefined以外的值,后面的钩子就都不执行了。
- 流水型钩子WaterHook:上一个钩子的返回值,作为下一个钩子的参数传递进去。 好了,到目前为止我们已经介绍了tapable中钩子的分类,可能大家不是很能够理解,接下来我会一个一个地带大家认识每一个钩子,并实现一遍。
Sync* 同步类型的钩子
SyncHook 普通型同步钩子
1、定义: SyncHook是普通型同步钩子。各个钩子按照顺序依次执行,互不影响。
2、使用:
let {SyncHook} = require("tapable");
console.log(new SyncHook(["name"]); // SyncHook类实例身上都有什么。
我们在之前介绍过tapable的每个子类都是一个用于注册和触发事件的钩子,我们可以查看一下SyncHook实例身上有哪些属性,找到它注册事件和触发事件的属性。
SyncHook {
_args: [],
name: undefined,
taps: [],
interceptors: [],
_call: [Function: CALL_DELEGATE],
call: [Function: CALL_DELEGATE], // 看这里,看这里。用于触发同步事件的钩子
_callAsync: [Function: CALL_ASYNC_DELEGATE],
callAsync: [Function: CALL_ASYNC_DELEGATE],
_promise: [Function: PROMISE_DELEGATE],
promise: [Function: PROMISE_DELEGATE],
_x: undefined,
compile: [Function: COMPILE],
tap: [Function: tap], // 看这里,看这里。用于注册同步事件的钩子
tapAsync: [Function: TAP_ASYNC],
tapPromise: [Function: TAP_PROMISE],
constructor: [Function: SyncHook]
}
通过上面的属性,我们可以知道SyncHook
通过tap
注册事件,通过call
触发事件。接下来我们就使用SyncHook
来定义一个钩子,并触发它。
let {SyncHook} = require("tapable");
class Lesson{
constructor(){
this.hooks = {
arch:new SyncHook(["name"]), // SyncHook的实例
}
}
tap(){
this.hooks.arch.tap("js",(name) => { // 注册js事件
console.log(`${name}注册了js事件`);
});
this.hooks.arch.tap("css",(name) => { // 注册css事件
console.log(`${name}注册了css事件`);
});
}
start(){
this.hooks.arch.call("haiyinsitan"); // 触发,执行所有的事件。
}
}
let lesson = new Lesson();
lesson.tap(); // 注册这两个事件
lesson.start(); // 启动钩子
从上面的代码中,我们可以看到。通过this.hooks.arch.tap(xxx,fn)
用于注册一个事件,其中this.hooks.arch
是SyncHook
的实例。通过this.hooks.arch.call
触发所有的事件。最终的输出结果是:
haiyinsitan注册了js事件
haiyinsitan注册了css事件
我们总结一下SyncHook
的特点:
- 同步钩子各个事件同时执行
- 各个事件之间互不影响,不存在参数传递,和阻碍执行的情况
- call触发时执行所有的事件,传递的参数,传递给了每个事件。
基于以上的特点,我们实现一个自定义的SyncHook。
自定义的SyncHook
class MySyncHook{
constructor(args){
this.tasks = [];
}
tap(eventName,task){
this.tasks.push(task)
}
call(...args){
this.tasks.forEach((task) => {
task(...args)
})
}
}
我们可以看下,实现思路其实非常简单,只需要定义一个队列,每次通过tap注册事件时,将执行的函数放入进去。当通过call进行调用时,一次性执行队列中所有的函数即可。在代码中使用进行查看:
class MySyncHook{
constructor(args){
this.tasks = [];
}
tap(eventName,task){
this.tasks.push(task)
}
call(...args){
this.tasks.forEach((task) => {
task(...args)
})
}
}
class Lesson {
constructor() {
this.hooks = {
arch: new MySyncHook(["name"]),
}
}
tap() {
console.log(Object.keys(this.hooks.arch));
this.hooks.arch.tap("js", (name) => {
console.log(name);
});
this.hooks.arch.tap("css", (name) => {
console.log(name);
});
}
start() {
this.hooks.arch.call("haiyinsitan");
}
}
let lesson = new Lesson();
lesson.tap(); // 注册这两个事件
lesson.start(); // 启动钩子
最终的效果与原来的SyncHook一致。当然,这种实现只是简单的功能上的一致。
SyncBailHook熔断型同步钩子
1、定义:
Sync:同步。Bail:保险。SyncBailHook表示出于安全考虑,直接熔断了,炒过股票的同学可能都知道股市存在熔断,也就是下跌超过某个值,比如10%就直接熔断,所有股票禁止交易。在这里也是一样,熔断型的钩子,如果返回非undefined的值到这里执行完毕就直接熔断了。
2、使用:
let { SyncBailHook} = require("tapable");
class Lesson {
constructor() {
this.hooks = {
arch: new SyncBailHook(["name"]),
}
}
tap() {
this.hooks.arch.tap("js", (name) => {
console.log(`${name}在学习js课程`); // 没有返回值,默认返回undefined
});
this.hooks.arch.tap("css", (name) => {
console.log(`${name}在学习css课程`);
return "非undefined就停止后面的钩子运行"; // 返回非undefined的值,下面的钩子就都不执行了。
});
this.hooks.arch.tap("html", (name) => {
console.log(`${name}在学习html课程`);
});
}
start() {
this.hooks.arch.call("haiyinsitan");
}
}
let lesson = new Lesson();
lesson.tap(); // 注册这两个事件
lesson.start(); // 启动钩子
我们可以看下,第一个钩子注册的js
事件,调用的函数没有返回值,默认返回undefined,第二个钩子注册的css
事件,返回一个非undefined的字符串。第三个钩子注册了html
事件。
我们看下最终的输出结果:
haiyinsitan在学习js课程
haiyinsitan在学习css课程
我们可以发现,只执行了两个事件的执行函数,最后一个事件html
由于上一个css
事件返回了非undefiend的值,因此直接熔断了,后面的钩子都不执行了。这就是SyncBailHook的特点。
自定义的SyncBailHook
我们可以发现SyncBailHook和SyncHook的最重要的区别就是,如果上一个钩子的返回值是非undefined,那么后面的钩子都无法执行。因此,我们只需要对每个钩子执行后的返回值进行判断,如果它不等于undefined,只需要阻止后面的钩子执行即可。
class MySyncBailHook {
constructor(args) {
this.tasks = [];
}
tap(eventName, task) {
this.tasks.push(task)
}
call(...args) {
for(let i = 0;i < this.tasks.length;i++){
let result = this.tasks[i](...args);
if(result !== undefined){ // 看这里,看这里。如果上一个钩子的返回值不等于undefined
break; // 终止循环,不让后面的钩子执行。
}
}
}
}
SyncWaterfallHook流水型同步钩子
1、定义:
SyncWaterfallHook流水型同步钩子:上一个钩子的执行结果,作为下一个钩子的参数进行传递。
2、使用:
let { SyncWaterfallHook } = require("tapable");
class Lesson {
constructor() {
this.hooks = {
arch: new SyncWaterfallHook(["name"]),
}
}
tap() {
this.hooks.arch.tap("js", (name) => {
console.log("js课程");
return "js课程";
});
this.hooks.arch.tap("node", (data) => {
console.log(`上一个钩子返回:${data}`); // 拿到上一个钩子的返回值,作为参数
console.log("node课程");
});
}
start() {
this.hooks.arch.call();
}
}
let lesson = new Lesson();
lesson.tap(); // 注册这两个事件
lesson.start(); // 启动钩子
我们可以看下,最终的输出值:
js课程
上一个钩子返回:js课程
node课程
其中node
钩子的参数是上一个钩子js
的返回值。这也是SyncWaterfallHook的最大特点。
自定义SyncWaterfallHook
SyncWaterfallHook钩子的最大特点就是:需要拿到上一个钩子的返回值,作为下一个钩子的参数。其中第一个钩子的参数是call时传入的值。其他钩子都是上一个钩子的返回值。因此我们只需要对参数做一下特殊处理即可。
class MySyncWaterfallHook {
constructor(args) {
this.tasks = [];
}
tap(eventName, task) {
this.tasks.push(task)
}
call(...args) {
let result = null;
for (let i = 0; i < this.tasks.length; i++) {
if(i === 0){
result = this.tasks[i](...args); // 第一个钩子参数是传入的值
}else{
result = this.tasks[i](result); // 其他钩子参数是上一个钩子的返回值
}
}
}
}
Async*异步类型的钩子
tapable可以注册各种各样的事件,但是这些事件的执行函数不可能都是同步的,有时候我们不可避免地需要使用到异步。而异步函数我们不知道它什么时候能够执行完毕,因此需要一个callback参数来标识异步函数执行完毕。同理钩子的触发也不知道所有的异步函数什么时候能够执行完毕,因此也需要通过callback来标识所有异步钩子执行完毕。
AsyncParallelHook异步并行钩子
1、定义:
AsyncParallelHook:异步并行钩子。
异步:钩子注册的事件的回调函数是一个异步的函数。
并行执行:所有的钩子虽然都是异步的但是同时执行,不分先后。
2、使用:
let { AsyncParallelHook} = require("tapable");
class Lesson {
constructor() {
this.hooks = {
arch: new AsyncParallelHook(["name"]),
}
}
tap() {
this.hooks.arch.tapAsync("js", (name,callback) => { // 使用tapAsync来注册事件
setTimeout(() => {
console.log("js课程");
callback();
},1000)
});
this.hooks.arch.tapAsync("css", (name,callback) => {
setTimeout(() => {
console.log("css课程");
callback();
},1000)
});
}
start() {
this.hooks.arch.callAsync("haiyinsitan",() => { // 使用callAsync来触发钩子
console.log("异步钩子在所有异步hooks执行完毕之后,必须有回调")
});
}
}
let lesson = new Lesson();
lesson.tap(); // 注册这两个事件
lesson.start(); // 启动钩子
在上面的代码中我们可以看到异步钩子和同步钩子的区别:
-
异步钩子使用tapAsync来注册事件,使用callAsync来触发事件
-
异步钩子注册的执行函数最后一个参数是callback,这个callback用来标识异步函数执行完毕
this.hooks.arch.tapAsync("css", (name,callback) => { setTimeout(() => { console.log("css课程"); callback(); // callback必须执行 },1000) });
-
callAsync的执行函数的最后一个参数是一个回调函数,在所有异步钩子执行完毕之后执行。
我们可以看下最终的输出结果:
js课程 // 并行执行,1000s后执行
css课程 // 并行执行,1000s后执行
异步钩子在所有异步hooks执行完毕之后,必须有回调 // 所有异步函数执行完毕之后执行
自定义AsyncParallelHook
AsyncParallelHook的特点:
- 异步执行,需要有callback参数
- 并行执行,所有钩子对应的函数同时执行
- callAsync的最后一个参数是在所有异步钩子执行完毕之后执行
class MyAsyncParallelHook {
constructor(args) {
this.tasks = [];
}
tapAsync(eventName, task) {
this.tasks.push(task)
}
callAsync(...args) {
let finalCallback = args.pop(); //最后一个参数是回调函数。
let index = 0; // index用来标识有多少异步钩子执行了。
const done = () => { // done就是每个异步函数的callback
index += 1;
if (index === this.tasks.length) {
finalCallback(); // 所有异步钩子执行完毕之后执行
}
}
this.tasks.forEach((task) => {
task(...args,done);
});
}
}
AsyncSeriesHook异步串行钩子
1、定义:
AsyncSeriesHook:异步串行钩子。
异步:钩子注册的事件的回调函数是一个异步的函数。
串行执行:钩子按照注册的顺序依次执行。
2、使用:
let { AsyncSeriesHook} = require("tapable");
class Lesson {
constructor() {
this.hooks = {
arch: new AsyncSeriesHook(["name"]),
}
}
tap() {
this.hooks.arch.tapAsync("js", (name, callback) => {
setTimeout(() => {
console.log("5000ms后执行js课程");
callback();
}, 5000)
});
this.hooks.arch.tapAsync("css", (name, callback) => {
setTimeout(() => {
console.log("1000ms后执行css课程");
callback();
}, 1000)
});
}
start() {
this.hooks.arch.callAsync("haiyinsitan", () => {
console.log("异步钩子在所有异步hooks执行完毕之后,必须有回调")
});
}
}
let lesson = new Lesson();
lesson.tap(); // 注册这两个事件
lesson.start(); // 启动钩子
我们可以发现:注册的js事件的执行函数,需要再5000ms之后执行,而注册的css
事件只在1000ms之后执行。然后查看一下输出结果:
5000ms后执行js课程
1000ms后执行css课程
异步钩子在所有异步hooks执行完毕之后,必须有回调
发现,先注册的js事件,虽然执行时间长,但是还是先执行了,也就是说AsyncSeriesHook是按照注册的先后顺序串行执行的。
自定义AsyncSeriesHook
AsyncSeriesHook的特点:
- 异步执行
- 串行执行。按照注册的顺序依次执行
所有的异步串行执行(有先后顺序的)的函数,都需要一个中间函数next
。next会作为每个函数的最后一个参数传递进去,然后执行。
class MyAsyncSeriesHook {
constructor(args) {
this.tasks = [];
}
tapAsync(eventName, task) {
this.tasks.push(task)
}
callAsync(...args) {
let finalCallback = args.pop();
let index = 0;
let next = () => {
if(this.tasks.length === index){
finalCallback();
return;
}
let task = this.tasks[index++];
task(...args,next);
}
next();
}
}
AsyncSeriesWaterfallHook异步串行流水型钩子
1、定义:
AsyncSeriesWaterfallHook:异步串行钩子。
异步:钩子注册的事件的回调函数是一个异步的函数。
串行执行:钩子按照注册的顺序依次执行。
流水型:上一个钩子的返回值作为下一个钩子的参数。
2、使用
let { AsyncSeriesWaterfallHook} = require("tapable");
class Lesson {
constructor() {
this.hooks = {
arch: new AsyncSeriesWaterfallHook(["name"]),
}
}
tap() {
this.hooks.arch.tapAsync("js", (data, callback) => {
setTimeout(() => {
console.log("js课程");
// callback("error","js课程"); // 第一个值是error,如果为null那么会继续往下执行。
callback(null,"js课程");
}, 2000)
});
this.hooks.arch.tapAsync("css", (data, callback) => {
setTimeout(() => {
console.log("上一个钩子的返回结果:",data)
console.log("css课程");
callback("error","css课程"); // 看这里,看这里。
}, 1000)
});
this.hooks.arch.tapAsync("node", (data, callback) => {
setTimeout(() => {
console.log("上一个钩子的返回结果:",data)
console.log("node课程");
callback();
}, 1000)
});
}
start() {
this.hooks.arch.callAsync("haiyinsitan", () => {
console.log("异步钩子在所有异步hooks执行完毕之后,必须有回调")
});
}
}
let lesson = new Lesson();
lesson.tap(); // 注册这两个事件
lesson.start(); // 启动钩子
注意:异步流水型的钩子,通过callback进行参数传递,这里我们需要注意一下callback的参数:
- callback的第一个参数用来标识错误。如果为null标识没有错误,为其他值表示有错误,当有错误时,直接熔断,后面的钩子不执行。
- callback的除了第一个参数以外的参数用于传递值。
我们可以看下上面代码的最终的输出结果:
js课程
上一个钩子的返回结果: js课程
css课程
异步钩子在所有异步hooks执行完毕之后,必须有回调
可以发现:css钩子顺利拿到了上一个钩子的返回值,但是node钩子由于css钩子callback的第一个参数为非null的值,因此直接熔断,之后的所有钩子都不执行了。
自定义AsyncSeriesWaterfallHook
AsyncSeriesHook的特点:
- 异步执行
- 串行执行。按照注册的顺序依次执行
- 流水型:上一个钩子的返回值作为下一个钩子的参数
class MyAsyncSeriesWaterfallHook {
constructor(args) {
this.tasks = [];
}
tapAsync(eventName, task) {
this.tasks.push(task)
}
callAsync(...args) {
let index = 0;
let finalCallback = args.pop();
let next = (error,data) => {
let task = this.tasks[index];
if(!task){
return finalCallback();
}
if(index === 0){
task(...args,next);
}else{
task(data,next);
}
index += 1;
}
next();
}
}
注意:上面的代码中没有对错误的处理。只是实现了AsyncSeriesWaterfallHook的上述的三个特点。
总结
到目前为止,本篇文章主要介绍了:
- tapable是什么?
- tapable的各个钩子以及他们的分类和特点
- 详细介绍了每种钩子的使用,并实现了它的主要功能
文中并没有介绍和实现所有的钩子,因为有些钩子在webpack中的使用次数为0。我们并不需要去关心它,而且通过它们的名字基本上就能够知道他们的用途了。掌握了这些钩子,我们再去看webpack的源码就能够事半功倍。