Webpack
的成功之处,不只在于强大的打包构建能力,也在于它灵活的插件机制。javascript
Webpack本质上是一种事件流的机制,它的工做流程就是将各个插件串联起来,而实现这一切的核心就是Tapable。java
在学习Webpack
的时候,常常能够看到上述介绍。也就是说学Webpack
的前提是要学习Tapable
。才能更好的学习Webpack
原理。node
其实tapable
的核心思路有点相似于node.js
中的events
,最基本的发布/订阅模式。webpack
const EventEmitter = require('events');
const myEmitter = new EventEmitter();
// 注册事件对应的监听函数
myEmitter.on('start', (params) => {
console.log("输出", params)
});
// 触发事件 并传入参数
myEmitter.emit('start', '学习webpack工做流'); // 输出 学习webpack工做流
复制代码
首先,tapable
提供的钩子有以下10个。 web
const {
SyncHook,
SyncBailHook,
SyncWaterfallHook,
SyncLoopHook,
AsyncParallelHook,
AsyncParallelBailHook,
AsyncSeriesHook,
AsyncSeriesBailHook,
AsyncSeriesLoopHook,
AsyncSeriesWaterfallHook
} = require("tapable");
复制代码
其次,全部钩子的用法简介,以下:(能够简单瞄一眼,就往下看吧)数组
序号 | 钩子名称 | 执行方式 | 使用要点 |
---|---|---|---|
1 | SyncHook | 同步串行 | 不关心监听函数的返回值 |
2 | SyncBailHook | 同步串行 | 只要监听函数中有一个函数的返回值不为 undefined,则跳过剩下全部的逻辑 |
3 | SyncWaterfallHook | 同步串行 | 上一个监听函数的返回值能够传给下一个监听函数 |
4 | SyncLoopHook | 同步循环 | 当监听函数被触发的时候,若是该监听函数返回true时则这个监听函数会反复执行,若是返回 undefined 则表示退出循环 |
5 | AsyncParallelHook | 异步并发 | 不关心监听函数的返回值 |
6 | AsyncParallelBailHook | 异步并发 | 只要监听函数的返回值不为 null,就会忽略后面的监听函数执行,直接跳跃到callAsync等触发函数绑定的回调函数,而后执行这个被绑定的回调函数 |
7 | AsyncSeriesHook | 异步串行 | 不关心callback()的参数 |
8 | AsyncSeriesBailHook | 异步串行 | callback()的参数不为null,就会直接执行callAsync等触发函数绑定的回调函数 |
9 | AsyncSeriesWaterfallHook | 异步串行 | 上一个监听函数的中的callback(err, data)的第二个参数,能够做为下一个监听函数的参数。 |
10 | AsyncSeriesLoopHook | 异步串行 | 能够触发handler循环调用。 |
同步串行,不关心监听函数的返回值。bash
咱们先来介绍最简单的SyncHook
,其实每一个Hook
都大同小异,懂一个其余的就很是好懂了。并发
const {SyncHook} = require("tapable");
//全部的构造函数都接收一个可选的参数,这个参数是一个字符串的数组。
let queue = new SyncHook(['param1']);
// 订阅tap 的第一个参数是用来标识订阅的函数的
queue.tap('event 1', function (param1) {
console.log(param1, 1);
});
queue.tap('event 2', function (param1) {
console.log(param1, 2);
});
queue.tap('event 3', function () {
console.log(3);
});
// 发布的时候触发订阅的函数 同时传入参数
queue.call('hello');
// 控制台输出
/* hello 1 hello 2 3 */
复制代码
能够看到,这个钩子订阅的事件都是按顺序同步执行的。app
简单模拟下原理。异步
class SyncHook{
constructor(){
this.taps = [];
}
// 订阅
tap(name, fn){
this.taps.push(fn);
}
// 发布
call(){
this.taps.forEach(tap => tap(...arguments));
}
}
复制代码
再来看下SyncBailHook
的使用。
只要监听函数中有一个函数的返回值不为undefined,则跳过剩下全部的逻辑。
let queue = new SyncBailHook(['param1']); //全部的构造函数都接收一个可选的参数,这个参数是一个字符串的数组。
// 订阅
queue.tap('event 1', function (param1) {// tap 的第一个参数是用来标识订阅的函数的
console.log(param1, 1);
return 1;
});
queue.tap('event 2', function (param1) {
console.log(param1, 2);
});
queue.tap('event 3', function () {
console.log(3);
});
// 发布
queue.call('hello', 'world');// 发布的时候触发订阅的函数 同时传入参数
// 控制台输出
/* hello 1 */
复制代码
能够看到,只要监听函数中有一个函数的返回值不为undefined
,则跳过剩下全部的逻辑。
简单模拟下原理。
class SyncBailHook {
constructor() {
this.taps = [];
}
// 订阅
tap(name, fn) {
this.taps.push(fn);
}
// 发布
call() {
for (let i = 0, l = this.taps.length; i < l; i++) {
let tap = this.taps[i];
let result = tap(...arguments);
if (result) {
break;
}
}
}
}
复制代码
上述2
种的钩子的执行流程以下图所示:
2
个钩子的介绍,能够发现
tapable
提供了各类各样的
hook
来帮咱们管理事件是如何执行的。
tapable
的核心功能就是控制一系列注册事件之间的执行流控制,好比我注册了三个事件,我能够但愿他们是并发的,或者是同步依次执行,又或者其中一个出错后,后面的事件就不执行了,这些功能均可以经过tapable
的hook
实现。
就像起床、上班、吃早饭的关系同样,起床确定是优先的。可是吃饭和上班就不必定啦。万一要迟到了呢?可能就放弃早饭了!
记住重点,核心就是call
和tap
两个方法。
记住重点,核心就是call
和tap
两个方法。
记住重点,核心就是call
和tap
两个方法。
那咱们来看下tapable
源码的SyncHook
是如何实现的,以下。仍是那句话,看完一个,其余的天然就懂啦。为了理解,源码均为缩减过的,去除了些非核心代码。
// node_modules/tapable/lib/SyncHook.js
const factory = new SyncHookCodeFactory();
// 继承基础Hook类
class SyncHook extends Hook {
// 重写Hook的compile方法
compile(options) {
// 开发者订阅的事件传
factory.setup(this, options);
// 动态生成call方法
return factory.create(options);
}
}
module.exports = SyncHook;
复制代码
核心代码很是简单,能够看到SyncHook
就是继承了Hook
基础类。并重写了compile
方法。
首先来看下Hook
基础类的tap
方法。能够看到每次调用tap
,就是收集当前hook
实例全部订阅的事件到taps
数组。
// node_modules/tapable/lib/Hook.js
// 订阅
tap(options, fn) {
// 同步 整理配置项
options = Object.assign({ type: "sync", fn: fn }, options);
// 将订阅的事件存储在taps里面
this._insert(options);
}
_insert(item) {
// 将item 推动 this.taps
this.taps[i] = item;
}
复制代码
而后来看下Hook
基础类的call
方法是如何实现的。
// node_modules/tapable/lib/Hook.js
class Hook {
constructor(args) {
this.taps = [];
this.call = this._call;
}
compile(options) {
// 继承类必须重写compile
throw new Error("Abstract: should be overriden");
}
// 执行compile生成call方法
_createCall(type) {
return this.compile({
taps: this.taps,
// ...等参数
});
}
}
// 动态生成call方法
function createCompileDelegate(name, type) {
return function lazyCompileHook(...args) {
// 创造call等函数
this[name] = this._createCall(type);
// 执行触发call等函数
return this[name](...args);
};
}
// 定义_call方法
Object.defineProperties(Hook.prototype, {
_call: {
value: createCompileDelegate("call", "sync"),
configurable: true,
writable: true
},
});
复制代码
经过上述代码,咱们能够发现,call
方法到底是什么,是经过重写的compile
方法生成出来的。那咱们再看下compile
方法究竟作了什么。
先来看下SyncHook
的所有代码。
// node_modules/tapable/lib/SyncHook.js
const Hook = require("./Hook");
const HookCodeFactory = require("./HookCodeFactory");
// 继承工厂类
class SyncHookCodeFactory extends HookCodeFactory {
// call方法个性化定制
content({ onError, onDone, rethrowIfPossible }) {
return this.callTapsSeries({
onError: (i, err) => onError(err),
onDone,
rethrowIfPossible
});
}
}
const factory = new SyncHookCodeFactory();
// 继承基础Hook类
class SyncHook extends Hook {
// 重写Hook的compile方法
compile(options) {
// 开发者订阅的事件传
factory.setup(this, options);
// 动态生成call方法
return factory.create(options);
}
}
module.exports = SyncHook;
复制代码
能够看到compile
主要是执行factory
的方法,而factory
是SyncHookCodeFactory
的实例,继承了HookCodeFactory
类,而后factory
实例调用了setup
方法。
setup
就是将taps
中订阅的事件方法统一给了this._x
;
// node_modules/tapable/lib/HookCodeFactory.js
setup(instance, options) {
// 将taps里的全部fn 赋值给 _x
instance._x = options.taps.map(t => t.fn);
}
复制代码
而后再看下factory
实例调用的create
方法。
// node_modules/tapable/lib/HookCodeFactory.js
create(options) {
this.init(options);
let fn;
switch (this.options.type) {
case "sync":
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
})
);
break;
}
}
复制代码
create
会将传进来的全部事件,进行组装。最终生成call
方法。 以下就是咱们此次的案例最终生成的call
方法。
function anonymous(param1) {
"use strict";
var _context;
var _x = this._x;
var _fn0 = _x[0];
_fn0(param1);
var _fn1 = _x[1];
_fn1(param1);
var _fn2 = _x[2];
_fn2(param1);
}
复制代码
若是你订阅了5
个事件,上述代码就会变成5
个函数的依次执行。以及参数必须是建立hook
实例就声明好的。不然tap
事件传的参数是无用的~
以上代码仍是简写了不少,你们能够直接去看下源码,很是精简好理解。给做者大大点赞。👍
总结一下,核心就是call
和tap
两个方法。其实还有tapAsync
等...可是原理都是同样的。tap
收集订阅的事件,触发call
方法时根据hook
的种类动态生成对应的执行体。以下图,其余hook
的实现也是同理。
Webpack
的流程能够分为如下三大阶段:
执行webpack
时,会生成一个compiler
实例。
// 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
都是各类各样的钩子。
// 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
。
// 订阅在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
。
虽然插件化设计很灵活,咱们能够写插件操做webpack
的整个生命周期。可是也发现插件化设计带来的一些问题,就是阅读源码很是很差的体验:
(1)联系松散。使用tapable
钩子相似事件监听模式,虽然能有效解耦,但钩子的注册与调用几乎没有联系。
(2)看到源码里一个模块提供了几个钩子,但并不知道,在什么时候、何地该钩子会被调用,又在什么时候、何地钩子上被注册了哪些方法。这些以往都是须要经过在代码库中搜索关键词来解决。
(3)钩子数量众多。webpack
内部的钩子很是多,数量达到了180+
,
本篇文主要是讲原理,理解tapable
。其余的钩子的使用,能够看这篇文章。