🍻 前言
Webpack
的成功之处,不仅在于强大的打包构建能力,也在于它灵活的插件机制。
Webpack
的插件机制本质上是一种事件流的机制,它的工作流程就是将各个插件串联起来,而实现这一切的核心就是 Tapable
,Webpack
中最核心的负责编译的 Compiler
和负责创建bundles的 Compilation
都是 Tapable
的实例。
🍻 所谓 Tapable?
Tapable
是一个类似于 Node.js 中的 EventEmitter 的库,但它更专注于自定义事件的触发和处理。通过 Tapable
我们可以注册自定义事件,然后在适当的时机去执行自定义事件。类比到 Vue
和 React
框架中的生命周期函数,它们就是到了固定的时间节点就执行对应的生命周期。
以syncHook 为例实操一下
初始化项目,安装依赖:
js
npm init //初始化项目
yarn add tapable -D //安装依赖
根据以下目录来添加对于对应的目录和文件
js
├── node_modules
├── package-lock.json
├── package.json
└── src # 源码目录
└── syncHookDemo.js
使用 tapable
步骤:
🔸 实例化钩子函数(tapable
会暴露出各种各样的 Hook
,这里先以同步钩子 Synchook
为例)
🔸 注册事件
🔸 触发事件
src/syncHookDemo.js
js
const SyncHook = require("../my/SyncHook");//同步钩子
// 第一步:实例化钩子函数,可以在这里定义形参
const syncHook = new SyncHook(['author', 'age']);
// 第二步:注册事件1
syncHook.tap("监听器1", (name, age) => {
console.log('监听器1:', name, age);
})
// 第二步:注册事件2
syncHook.tap("监听器2", (name) => {
console.log('监听器2:', name);
})
// 第三步:注册事件3
syncHook.tap("监听器3", (name) => {
console.log('监听器3:', name);
})
// 第三步:触发事件,这里传的是实参,会被每一个注册函数接收到
syncHook.call('嘎嘎嘎', '9');
运行 node ./src/syncHookDemo.js
,拿到执行结果:
js
监听器1 嘎嘎嘎 99
监听器2 嘎嘎嘎
监听器3 嘎嘎嘎
可见,tapable
采用的是发布订阅方式,通过 tap
函数注册监听函数,然后通过 call
函数按顺序执行之前注册的函数。
大致的原理:
js
class SyncHook {
contrustor() {
this.taps = [];
}
// 注册监听函数,这里的name其实没啥用
tap(name, fn) {
this.taps.push({name, fn})
}
// 执行函数
call(...args) {
this.taps.foEach((tap) => tap.fn(...args));
}
}
🍻 Tapable 钩子分类
🚖 按照同步/异步分类
对同步钩子来说,tap
方法是唯一注册事件的方法,通过 call 方法触发同步钩子的执行。
对于异步钩子来说,可以通过 tap
、tapAsync
、tapPromise
三种方式来注册,通过对应的 callAsync
、promise
这两种方式来触发注册的函数。
🚖 按照执行机制分类
🔸 Basic Hook
基本类型的钩子,执行每一个注册的事件函数,并不关心每个被调用的事件函数返回值如何
🔸 Waterfall
瀑布类型的钩子,如果前一个事件函数的结果 result !== undefined
,则 result
会作为后一个事件函数的第一个参数
上一个函数的执行结果会成为下一个函数的参数
🔸 Bail
保险类钩子,执行每一个事件函数,遇到第一个结果 result !== undefined
则返回,不再继续执行
其中有一个有结果后面的就不执行
🔸 Loop
循环类钩子,不停地循环执行事件函数,直到所有函数结果 result === undefined
🍻 基本使用
🚖 SyncHook
上面的案例就是基于 SyncHook
,就不再赘述。
🚖 SyncBailHook
SyncBailHook
是一个同步的、保险类型的 Hook
,意思是只要其中一个有返回了,后面的就不执行了。
js
const { SyncBailHook } = require("tapable");
const hook = new SyncBailHook(["author", "age"]); //先实例化,并定义回调函数的形参
//通过tap函数注册事件
hook.tap("测试1", (param1, param2) => {
console.log("测试1接收的参数:", param1, param2);
});
//该监听函数有返回值
hook.tap("测试2", (param1, param2) => {
console.log("测试2接收的参数:", param1, param2);
return "123";
});
hook.tap("测试3", (param1, param2) => {
console.log("测试3接收的参数:", param1, param2);
});
//通过call方法触发事件
hook.call("嘎嘎嘎", "99");
//结果:
测试1接收的参数: 嘎嘎嘎 99
测试2接收的参数: 嘎嘎嘎 99
🚖 SyncWaterfallHook
SyncWaterfallHook
是一个同步的、瀑布式类型的 Hook
。瀑布类型的钩子就是如果前一个事件函数的结果result !== undefined
,则 result
会作为后一个事件函数的第一个参数(也就是上一个函数的执行结果会成为下一个函数的参数)
js
const { SyncWaterfallHook } = require("tapable");
const hook = new SyncWaterfallHook(["author", "age"]); //先实例化,并定义回调函数的形参
//通过tap函数注册事件
hook.tap("测试1", (param1, param2) => {
console.log("测试1接收的参数:", param1, param2);
});
hook.tap("测试2", (param1, param2) => {
console.log("测试2接收的参数:", param1, param2);
return "123";
});
hook.tap("测试3", (param1, param2) => {
console.log("测试3接收的参数:", param1, param2);
});
//通过call方法触发事件
hook.call("嘎嘎嘎", "99");
//结果
测试1接收的参数: 嘎嘎嘎 99
测试2接收的参数: 嘎嘎嘎 99
测试3接收的参数: 123 99
🚖 SyncLoopHook
SyncLoopHook
是一个同步、循环类型的 Hook
。循环类型的含义是不停的循环执行事件函数,直到所有函数结果result === undefined
,不符合条件就调头重新开始执行。
js
const { SyncLoopHook } = require("tapable");
const hook = new SyncLoopHook([]); //先实例化,并定义回调函数的形参
let count = 5;
//通过tap函数注册事件
hook.tap("测试1", () => {
console.log("测试1里面的count:", count);
if ([1, 2, 3].includes(count)) {
return undefined;
} else {
count--;
return "123";
}
});
hook.tap("测试2", () => {
console.log("测试2里面的count:", count);
if ([1, 2].includes(count)) {
return undefined;
} else {
count--;
return "123";
}
});
hook.tap("测试3", () => {
console.log("测试3里面的count:", count);
if ([1].includes(count)) {
return undefined;
} else {
count--;
return "123";
}
});
//通过call方法触发事件
hook.call();
//结果
测试1里面的count: 5
测试1里面的count: 4
测试1里面的count: 3
测试2里面的count: 3
测试1里面的count: 2
测试2里面的count: 2
测试3里面的count: 2
测试1里面的count: 1
测试2里面的count: 1
测试3里面的count: 1
🚖 AsyncParallelHook
AsyncParallelHook
是一个异步并行、基本类型的 Hook
,它与同步 Hook
不同的地方在于:
- 它会同时开启多个异步任务,而且需要通过
tapAsync
方法来注册事件(同步Hook
是通过tap
方法) - 在执行注册事件时需要使用
callAsync
方法来触发(同步Hook
使用的是call
方法)
同时,在每个注册函数的回调中,会多一个 callback
参数,它是一个函数。执行 callback
函数相当于告诉 Hook
它这一个异步任务执行完成了。
js
const { AsyncParallelHook } = require("tapable");
const hook = new AsyncParallelHook(["author", "age"]); //先实例化,并定义回调函数的形参
console.time("time");
//异步钩子需要通过tapAsync函数注册事件,同时也会多一个callback参数,执行callback告诉hook该注册事件已经执行完成
hook.tapAsync("测试1", (param1, param2, callback) => {
setTimeout(() => {
console.log("测试1接收的参数:", param1, param2);
callback();
}, 2000);
});
hook.tapAsync("测试2", (param1, param2, callback) => {
console.log("测试2接收的参数:", param1, param2);
callback();
});
hook.tapAsync("测试3", (param1, param2, callback) => {
console.log("测试3接收的参数:", param1, param2);
callback();
});
//call方法只有同步钩子才有,异步钩子得使用callAsync
hook.callAsync("嘎嘎嘎", "99", (err, result) => {
//等全部都完成了才会走到这里来
console.log("这是成功后的回调", err, result);
console.timeEnd("time");
});
// 结果
测试2接收的参数: 嘎嘎嘎 99
测试3接收的参数: 嘎嘎嘎 99
测试1接收的参数: 嘎嘎嘎 99
这是成功后的回调 undefined undefined
time: 2.008s
🚖 AsyncParallelBailHook
AsyncParallelBailHook
是一个异步并行、保险类型的 Hook
,只要其中一个有返回值,就会执行 callAsync
中的回调函数
js
const { AsyncParallelBailHook } = require("tapable");
const hook = new AsyncParallelBailHook(["author", "age"]); //先实例化,并定义回调函数的形参
console.time("time");
//异步钩子需要通过tapAsync函数注册事件,同时也会多一个callback参数,执行callback告诉hook该注册事件已经执行完成
hook.tapAsync("测试1", (param1, param2, callback) => {
console.log("测试1接收的参数:", param1, param2);
setTimeout(() => {
callback();
}, 1000);
});
hook.tapAsync("测试2", (param1, param2, callback) => {
console.log("测试2接收的参数:", param1, param2);
setTimeout(() => {
callback(null, "测试2有返回值啦");
}, 2000);
});
hook.tapAsync("测试3", (param1, param2, callback) => {
console.log("测试3接收的参数:", param1, param2);
setTimeout(() => {
callback(null, "测试3有返回值啦");
}, 3000);
});
hook.callAsync("嘎嘎嘎", "99", (err, result) => {
//等全部都完成了才会走到这里来
console.log("这是成功后的回调", result);
console.timeEnd("time");
});
// 结果
测试1接收的参数: 嘎嘎嘎 99
测试2接收的参数: 嘎嘎嘎 99
测试3接收的参数: 嘎嘎嘎 99
这是成功后的回调 测试2有返回值啦
time: 2.007s
🚖 AsyncSeriesHook
AsyncSeriesHook
是一个异步、串行类型的 Hook
,只有前面的执行完成了,后面的才会一个接一个的执行。
js
const { AsyncSeriesHook } = require("tapable");
const hook = new AsyncSeriesHook(["author", "age"]); //先实例化,并定义回调函数的形参
console.time("time");
//异步钩子需要通过tapAsync函数注册事件,同时也会多一个callback参数,执行callback告诉hook该注册事件已经执行完成
hook.tapAsync("测试1", (param1, param2, callback) => {
console.log("测试1接收的参数:", param1, param2);
setTimeout(() => {
callback();
}, 1000);
});
hook.tapAsync("测试2", (param1, param2, callback) => {
console.log("测试2接收的参数:", param1, param2);
setTimeout(() => {
callback();
}, 2000);
});
hook.tapAsync("测试3", (param1, param2, callback) => {
console.log("测试3接收的参数:", param1, param2);
setTimeout(() => {
callback();
}, 3000);
});
hook.callAsync("嘎嘎嘎", "99", (err, result) => {
//等全部都完成了才会走到这里来
console.log("这是成功后的回调", err, result);
console.timeEnd("time");
});
// 结果
测试1接收的参数: 嘎嘎嘎 99
测试2接收的参数: 嘎嘎嘎 99
测试3接收的参数: 嘎嘎嘎 99
这是成功后的回调 undefined undefined
time: 6.017s
🚖 AsyncSeriesBailHook
AsyncSeriesBailHook
是一个异步串行、保险类型的 Hook
。在串行的执行过程中,只要其中一个有返回值,后面的就不会执行了。
js
const { AsyncSeriesBailHook } = require("tapable");
const hook = new AsyncSeriesBailHook(["author", "age"]); //先实例化,并定义回调函数的形参
console.time("time");
//异步钩子需要通过tapAsync函数注册事件,同时也会多一个callback参数,执行callback告诉hook该注册事件已经执行完成
hook.tapAsync("测试1", (param1, param2, callback) => {
console.log("测试1接收的参数:", param1, param2);
setTimeout(() => {
callback();
}, 1000);
});
hook.tapAsync("测试2", (param1, param2, callback) => {
console.log("测试2接收的参数:", param1, param2);
setTimeout(() => {
callback(null, "123");
}, 2000);
});
hook.tapAsync("测试3", (param1, param2, callback) => {
console.log("测试3接收的参数:", param1, param2);
setTimeout(() => {
callback();
}, 3000);
});
hook.callAsync("嘎嘎嘎", "99", (err, result) => {
//等全部都完成了才会走到这里来
console.log("这是成功后的回调", result);
console.timeEnd("time");
});
// 结果
测试1接收的参数: 嘎嘎嘎 99
测试2接收的参数: 嘎嘎嘎 99
这是成功后的回调 123
time: 3.010s
🚖 AsyncSeriesWaterfallHook
AsyncSeriesWaterfallHook
是一个异步串行、瀑布类型的 Hook
。如果前一个事件函数的结果 result !== undefined
,则 result
会作为后一个事件函数的第一个参数(也就是上一个函数的执行结果会成为下一个函数的参数)。
js
const { AsyncSeriesWaterfallHook } = require("tapable");
const hook = new AsyncSeriesWaterfallHook(["author", "age"]); //先实例化,并定义回调函数的形参
console.time("time");
//异步钩子需要通过tapAsync函数注册事件,同时也会多一个callback参数,执行callback告诉hook该注册事件已经执行完成
hook.tapAsync("测试1", (param1, param2, callback) => {
console.log("测试1接收的参数:", param1, param2);
setTimeout(() => {
callback(null, "2");
}, 1000);
});
hook.tapAsync("测试2", (param1, param2, callback) => {
console.log("测试2接收的参数:", param1, param2);
setTimeout(() => {
callback(null, "3");
}, 2000);
});
hook.tapAsync("测试3", (param1, param2, callback) => {
console.log("测试3接收的参数:", param1, param2);
setTimeout(() => {
callback(null, "4");
}, 3000);
});
hook.callAsync("嘎嘎嘎", "99", (err, result) => {
//等全部都完成了才会走到这里来
console.log("这是成功后的回调", err, result);
console.timeEnd("time");
});
// 结果
测试1接收的参数: 嘎嘎嘎 99
测试2接收的参数: 2 99
测试3接收的参数: 3 99
这是成功后的回调 null 4
time: 6.012s
🍻 Tapable 拦截器
Tapable
提供的所有 Hook
都支持注入 Interception
,它和 Axios
中的拦截器的效果非常类似。
我们可以通过拦截器对整个 Tapable
发布/订阅流程进行监听,从而触发对应的逻辑。
js
const hook = new SyncHook(['arg1', 'arg2', 'arg3']);
hook.intercept({
// 每次调用 hook 实例的 tap() 方法注册回调函数时, 都会调用该方法,
// 并且接受 tap 作为参数, 还可以对 tap 进行修改;
register: (tapInfo) => {
console.log(`${tapInfo.name} is doing its job`);
return tapInfo; // may return a new tapInfo object
},
// 通过hook实例对象上的call方法时候触发拦截器
call: (arg1, arg2, arg3) => {
console.log('Starting to calculate routes');
},
// 在调用被注册的每一个事件函数之前执行
tap: (tap) => {
console.log(tap, 'tap');
},
// loop类型钩子中 每个事件函数被调用前触发该拦截器方法
loop: (...args) => {
console.log(args, 'loop');
},
});
🔸 register
每次通过 tap
、tapAsync
、tapPromise
方法注册事件函数时,会触发 register
拦截器。这个拦截器中接受注册的 Tap
作为参数,同时可以对于注册的事件进行修改。
🔸 call
通过调用 hook
实例对象的 call
方法时执行。(包括 callAsync
, promise
)接受的参数为调用 Hook
时传入的参数。
🔸 tap
在每一个被注册的事件函数调用之前执行,接受参数为对应的 Tap
对象。
🔸 loop
loop
类型钩子中 每次重新开始 loop
之前会执行该拦截器,拦截器函数接受的参数为调用时传入的参数。
🍻 其他 API
关于 Tapable
其实还有相关的模块 API
分别是B efore && stage
、HookMap
、Context
。
🚖 Before & stage
Tapable 在注册事件函数时,第一个参数同时支持传入一个对象。
我们可以通过这个对象上的 stage 和 before 属性来控制本次注册的事件函数执行时机。
🔸 Before 属性
before
属性的值可以传入一个数组或者字符串,值为注册事件对象时的名称,它可以修改当前事件函数在传入的事件名称对应的函数之前进行执行。
比如:
js
const { SyncHook } = require('tapable');
const hooks = new SyncHook();
hooks.tap(
{
name: 'flag1',
},
() => {
console.log('This is flag1 function.');
}
);
hooks.tap(
{
name: 'flag2',
// flag2 事件函数会在flag1之前进行执行
before: 'flag1',
},
() => {
console.log('This is flag2 function.');
}
);
hooks.call();
// result
This is flag2 function.
This is flag1 function.
🔸 stage 属性
stage
这个属性的类型是数字,数字越大事件回调执行的越晚,支持传入负数,不传时默认为0.
js
const { SyncHook } = require('tapable');
const hooks = new SyncHook();
hooks.tap(
{
name: 'flag1',
stage: 1,
},
() => {
console.log('This is flag1 function.');
}
);
hooks.tap(
{
name: 'flag2',
// 默认为stage: 0,
},
() => {
console.log('This is flag2 function.');
}
);
hooks.call();
// result
This is flag2 function.
This is flag1 function.
注意:
如果同时使用 before
和 stage
时,优先会处理 before
,在满足 before
的条件之后才会进行 stage
的判断。
关于 before
和 stage
都可以修改事件回调函数的执行时间,但是不建议混用这两个属性。换句话说如果你选择在你的 hooks.tap
中使用 stage
的话就不要再出现 before
,反之亦然。
🚖 HookMap
HookMap
本质上就是一个辅助类,通过 HookMap
我们可以更好的管理 Hook
。
js
const { HookMap, SyncHook } = require('tapable');
// 创建HookMap实例
const keyedHook = new HookMap((key) => new SyncHook(['arg']));
// 在keyedHook中创建一个name为key1的hook,同时为该hook通过tap注册事件
keyedHook.for('key1').tap('Plugin 1', (arg) => {
console.log('Plugin 1', arg);
});
// 在keyedHook中创建一个name为key2的hook,同时为该hook通过tap注册事件
keyedHook.for('key2').tap('Plugin 2', (arg) => {
console.log('Plugin 2', arg);
});
// 在keyedHook中创建一个name为key1的hook,同时为该hook通过tap注册事件
keyedHook.for('key3').tap('Plugin 3', (arg) => {
console.log('Plugin 3', arg);
});
// 从HookMap中拿到name为key1的hook
const hook = keyedHook.get('key1');
if (hook) {
// 通过call方法触发Hook
hook.call('hello');
}
🚖 MultiHook
MultiHook
在日常应用中并不是很常见,它的主要作用也就是通过 MultiHook
批量注册事件函数在多个钩子中。
🚖 Context
关于 Context
在源码中如果你传递了 Context
参数,那么会进入这段逻辑:
js
const deprecateContext = util.deprecate(
() => {},
"Hook.context is deprecated and will be remove"
)
这个 API
将来会被废弃,一个即将废弃且使用场景不多的情况这里就不和大家展开讲解了。
🍻 Tapable 底层原理
将 Tapable
工程源码克隆到本地, 执行如下指令:
js
$ git clone https://github.com/webpack/tapable.git
Tapable
源码的 lib
目录结构, 如下所示:
js
lib
├─ AsyncParallelBailHook.js
├─ AsyncParallelHook.js
├─ AsyncSeriesBailHook.js
├─ AsyncSeriesHook.js
├─ AsyncSeriesLoopHook.js
├─ AsyncSeriesWaterfallHook.js
├─ Hook.js
├─ HookCodeFactory.js
├─ HookMap.js
├─ MultiHook.js
├─ SyncBailHook.js
├─ SyncHook.js
├─ SyncLoopHook.js
├─ SyncWaterfallHook.js
├─ index.js
└─ util-browser.js
除了上面我们所提及的基本 hooks
函数、HookMap
高级特性,还会有一些 HookCodeFactory
、Hook
这些文件。看完 hooks
函数内的内容, 会发现所有的 hooks
函数都会引用 HookCodeFactory
和 Hook
这两个文件所导出的对象实例。
以 syncHook
钩子函数为例, 如下图所示:
总结:
一个 hooks
函数 会由一个 CodeFactory
代码工厂 以及 Hook
实例组成。 Hook
实例会针对不同场景的 hooks
函数, 更改其对应的 注册钩子( tapAsync
, tap
, tapPromise
) ,事件触发钩子( call
, callAsync
) , 编译函数(complier) 。 Complier
函数会由我们 HookCodeFactory
实现。
接下来我们将通过分析
HookCodeFactory
及Hook
的内部实现来了解Tapable
的内部实现机制。
🚖 Hook 实例
Hook
实例会生成我们 hooks
钩子函数通用的 事件注册
,事件触发
。核心逻辑,我们大致可以分为:实例初始化构造函数、事件注册的实现、事件触发的实现这三个部分。
🔸(1)构造函数
构造函数会对实例属性初始化赋值。代码如下图所示:
🔸(2)注册事件
注册事件主要分为两块,一块是 适配器注册调用
, 第二块是 触发事件注册
。核心逻辑在 _tap
函数内部实现,代码如下图所示:
适配器调用
在这里会对携带 register
函数的适配器进行调用,更改 options
配置,返回新的 options
配置。代码如下图所示:
触发事件注册
Hook
实例的 taps
会存储我们的注册事件, 同时会根据,注册事件配置的执行顺序去存储对应的注册事件。
🔸(3)触发事件
触发事件会通过调用内部 的 _createCall
函数,函数内部会调用实例的 compile
函数。
Hook
实例内部不会去实现 complier
的逻辑, 不同钩子的 complier
函数会通过通过对应的 继承 HookCodeFactory
的实例去实现。代码如下图所示:
🚖 HookCodeFactory 实例
Tapable
事件触发的逻辑。
HookCodeFactory
实例会根据我们传入的事件触发类型 (sync, async, promise
)以及我们的触发机制类型 (常规
瀑布模式
保险模式
循环模式
),生成事件触发函数的函数头(header
)、函数体(content
)。通过 new Function
构造出事件触发函数。
其中:
content
最终会由 callTapsSeries
、callTapsLooping
、callTapsParallel
生成。每种生成方式都会包含 Done
处理、Error
处理以及 Result
处理。
Tapable
事件触发的执行,是动态生成执行代码, 包含我们的参数,函数头,函数体,然后通过new Function
来执行。相较于我们通常的遍历/递归调用事件,这无疑让webpack
的整个事件机制的执行有了一个更高的性能优势。
Hook
实例 的 complier
函数是 HookCodeFactory
实例 create
函数 的返回。
🔸 create
create
函数通过对应的 函数参数
, 函数 header
, 函数 content
方法构造出我们事件触发的函数的内容, 通过 new Function
创建触发函数。
create
函数会根据事件的触发类型 ( sync
、async
、promise
),进行不同的逻辑处理。代码如下图所示:
每一种触发机制,都会由 this.args
, this.header
, this.contentWithInterceptors
三个函数去实现 动态函数的 code
。代码如下图所示:
contentWithInterceptors
contentWithInterceptors
函数里包含两个模块, 一个是适配器 (interceptor
), 一个 content
生成函数。
同时,HookCodeFactory
实例本身不会去实现 content
函数的逻辑,会由继承的实例去实现。整体结构代码如下图所示:
每个 hooks
钩子函数的 CodeFactory
实例会去实现 content
函数。 content
函数会调用 HookCodeFactory
实现的不同运行机制的方法( callTap
、callTapsSeries
、callTapsLooping
、callTapsParallel
), 构造出最终的函数体。实现代码如下图所示:
接下来,就是不同运行机制,根据不同的调用方式 ( sync
, async
, promise
) 生成对应的执行代码。
🍻 mini 版的 SyncHook
具体实现
🚖 核心思想
这里以 SyncHook
的实现原理为例,其他的 Hook
也会整理一下思路,大家举一反三,重点在于理解思想。
回过头来看看 SyncHook
的用法,也就是这三步:
(1)实例化
(2)通过 tap
函数注册事件
(3)通过 call
方法触发事件
js
const SyncHook = require("../my/SyncHook");//同步钩子
// 第一步:实例化钩子函数,可以在这里定义形参
const syncHook = new SyncHook(['author', 'age']);
// 第二步:注册事件1
syncHook.tap("监听器1", (name, age) => {
console.log('监听器1:', name, age);
})
// 第二步:注册事件2
syncHook.tap("监听器2", (name) => {
console.log('监听器2:', name);
})
// 第三步:注册事件3
syncHook.tap("监听器3", (name) => {
console.log('监听器3:', name);
})
// 第三步:触发事件,这里传的是实参,会被每一个注册函数接收到
syncHook.call('嘎嘎嘎', '9');
其实 tap
函数就是一个收集器,当调用 tap
函数时需要将传入的这些信息进行收集,并转换成一个数组,数组里面存放着注册函数的类型type
、回调函数(fn)
等信息:
js
this.taps = [
{
name: "监听器1",
type: "sync",
fn: (param1, param2) => {
console.log("监听器1接收参数:", name, age);
},
},
{
name: "监听器2",
type: "sync",
fn: (param1, param2) => {
console.log("监听器2接收参数:", name);
},
},
]; //用来存放我们的回调函数基本信息
调用 call
函数的本质就是 按指定的类型
去执行 this.taps
中的注册函数 fn
,比如这里的 type: sync
,就是得按同步的方式执行,那我们只需将运行代码改造成这样:
js
function anonymous(param1, param2) {
const taps = this.taps;
let fn0 = taps[0].fn;
fn0(param1, param2);
let fn1 = taps[1].fn;
fn1(param1, param2);
}
anonymous("嘎嘎嘎", "99");
如果要按照SyncBailHook
(同步、保险类型:只要其中一个有返回值,后面的就不执行了 )执行,那我们只需将运行代码改造成这样:
js
function anonymous(param1, param2) {
const taps = this.taps;
let fn0 = taps[0].fn;
let result0 = fn0(param1, param2);
if (result0 !== undefined) {
return result0;
} else {
let fn1 = taps[1].fn;
let result1 = fn1(param1, param2);
if (result1 !== undefined) {
return result1;
}
}
}
anonymous("嘎嘎嘎", "99");
如果得按照 AsyncSeriesHook
(异步、串行类型:只有前面的执行完成了,后面的才会一个接一个的执行 )执行,那我们需要将运行代码改造成这样:
js
function anonymous(param1, param2, callback) {
const taps = this.taps;
let fn0 = taps[0].fn;
fn0(param1, param2, function (err) {
if (err) {
//如果运行过程中报错,则直接结束
callback(err);
} else {
next0();
}
});
function next0() {
let fn1 = taps[1].fn;
fn1(param1, param2, function (err) {
if (err) {
callback(err);
} else {
callback(); //在末尾执行最终的回调函数
}
});
}
}
anonymous("嘎嘎嘎", "99", (err,result)=>"最终的回调函数");
🚖 那如何生成运行函数呢?
官方的源码中是通过 new Function() 进行创建的,先了解一下 new Function
的语法:
js
let func = new Function ([arg1, arg2, ...argN], functionBody);
arg1, arg2, ... argN(参数名称)
:是一个有效的 JavaScript 字符串(例如:"a , b"),或者是一个字符串列表(例如:["a","b"])。functionBody(函数体)
:可执行的JavaScript字符串。
实例:
js
const sum = new Function("a,b", "return a + b");
console.log(sum(2, 6));
//output: 8
所以,以上面SyncHook
所需要的函数体为例:
该函数体其实可以分为两部分:
🔸 第一部分(header):获取存放着注册函数信息的数组 taps
js
const taps = this.taps;
🔸 第二部分(content):可以通过对 taps
进行遍历生成
js
let fn0 = taps[0].fn;
fn0(param1, param2);
let fn1 = taps[1].fn;
fn1(param1, param2);
现在通过new Function()
生成我们想要的执行函数
🔸 第一步:生成形参字符串("param1 , param2"
)
🔸 第二步:生成函数体中 header
部分
🔸 第三步:遍历 taps,生成 content
部分
js
new Function(
this.args().join(","),
this.header() + this.content()
);
核心思路就是这些,接下来实操!!
🚖 实操
首先需要通过 tap
函数进行收集工作,并将收集到的函数格式化
js
class SyncHook {
constructor(args) {
this.args = Array.isArray(args) ? args : [];// 形参列表
this.taps = [];// 用来存放注册函数的基本信息的数组
}
}
🔸(1) taps
的收集工作
这里分成两个小步骤,先对传入参数进行格式化。
使用 tap
的时候
js
hook.tap({name:"监听器1",后面还可以有其他参数}, callback);
因此要格式化处理
js
class SyncHook {
// ...
tap(option, fn) {
// 如果传入的是字符串,包装成对象
if (typeof option === 'string') {
option = {
name: option,
};
}
}
}
接着定义 tap
函数,收集注册函数信息
js
class SyncHook {
// ...
tap(option, fn) {
// 如果传入的是字符串,包装成对象
if (typeof option === 'string') {
option = {
name: option,
};
}
//type=sync fn是注册函数
const tapInfo = { ...option, type: "sync", fn};
this.taps.push(tapInfo);
}
}
🔸 (2)动态生成执行代码
当调用 call
方法时,会走两个关键的步骤:先动态生成执行代码,再执行生成的代码。
最终我们要通过 this.taps
生成如下格式的运行代码:
js
new Function(
"param1 , param2",
`
const taps = this.taps;
let fn0 = taps[0].fn;
fn0(param1, param2);
let fn1 = taps[1].fn;
fn1(param1, param2);
`
);
这一步需要遍历 this.taps
数组,然后生成对应的函数体字符串,这里封装成一个函数 compiler
来做:
js
class SyncHook {
// 省略其他
compile({args, taps, type}) {
const getHeader = () => {
let code = '';
code += `var taps = this.taps;\n`
}
const getContent = () => {
let code = '';
for (let i = 0; i < taps.length; i++) {
code += `var fn${i} = taps[${i}].fn;\n`;
code += `fn${i}(${args.join(',')});\n`;
}
return code;
}
return new Function(args.join(","), getHeader() + getContent());
}
}
🔸(3)执行生成的代码
这里是最后一步,定义 call
方法,然后执行生成的函数体:
js
class SyncHook {
// ...
call(...args) {
this._call = this.compile({
taps: this.taps,//tapInfo的数组 [{name,fn,type}]
args: this.args, //形参数组
type: "sync",
});//动态创建一个call方法 这叫懒编译或者动态编译,最开始没有,用的时候才去创建执行
return this._call(...args);
}
}
🍻 Tapable 在 webpack 中的应用
Webpack
的流程可以分为以下三大阶段:
🔸 初始化
启动构建,读取与合并配置参数,加载 Plugin,实例化 Compiler。这个 compiler 对象户会穿行在本次编译的整个周期
🔸 编译
从 Entry 发出,,针对每个 Modules 串行调用对应的 Loader 去翻译文件内容,再找到该 Module 依赖的 Module,递归地进行编译处理
🔸 输出文件
对编译后的 Module 组合成 Chunk,把 Chunk 转换成文件,输出到文件系统。
执行webpack
时,会生成一个compiler
实例。
js
// node_modules/webpack/lib/webpack.js
const Compiler = require("./Compiler");
const MultiCompiler = require("./MultiCompiler");
const webpack = (options, callback) => {
// ...省略了多余代码...
let compiler;
if (typeof options === "object") {
compiler = new Compiler(options.context);
} else {
throw new Error("Invalid argument: options");
}
})
可见,Compiler
是继承了Tapable
的。同时发现webpack
的生命周期hooks
都是各种各样的钩子。
js
// node_modules/webpack/lib/Compiler.js
class Compiler extends Tapable {
constructor(context) {
super();
this.hooks = {
/** @type {AsyncSeriesHook<Stats>} */
done: new AsyncSeriesHook(["stats"]),
/** @type {AsyncSeriesHook<>} */
additionalPass: new AsyncSeriesHook([]),
/** @type {AsyncSeriesHook<Compiler>} */
beforeRun: new AsyncSeriesHook(["compiler"]),
/** @type {AsyncSeriesHook<Compiler>} */
run: new AsyncSeriesHook(["compiler"]),
/** @type {AsyncSeriesHook<Compilation>} */
emit: new AsyncSeriesHook(["compilation"]),
/** @type {AsyncSeriesHook<string, Buffer>} */
assetEmitted: new AsyncSeriesHook(["file", "content"]),
/** @type {AsyncSeriesHook<Compilation>} */
afterEmit: new AsyncSeriesHook(["compilation"]),
// ....
}
}
}
然后在初始化webpack
的配置过程中,会循环我们配置的以及webpack
默认的所有插件也就是plugin
。
js
// 订阅在options中的所有插件
if (options.plugins && Array.isArray(options.plugins)) {
for (const plugin of options.plugins) {
if (typeof plugin === "function") {
plugin.call(compiler, compiler);
} else {
plugin.apply(compiler);
}
}
}
这个过程,会把 plugin
中所有 tap
事件收集到每个生命周期的 hook
中。 最后根据每个 hook
执行 call
方法的顺序(也就是生命周期)。就可以把所有 plugin
。
举个例子,下面是我们经常使用的热更新插件代码,它订阅了 additionalPass
等 hook
。
这也就是 webpack
它工作流程能将各个插件 plugin
串联起来的原因,而实现这一切的核心就是 Tapable
。