前言
Webpack 是一个现代 JavaScript 应用程序的静态模块打包器,是对前端项目实现自动化和优化必不可少的工具,Webpack 的 loader (加载器)和 plugin (插件)是由 Webpack 开发者和社区开发者共同贡献的,而目前又没有比较系统的开发文档,想写加载器和插件必须要懂 Webpack 的原理,即看懂 Webpack 的源码, tapable 则是 Webpack 依赖的核心库,可以说不懂 tapable 就看不懂 Webpack 源码,所以本篇会对 tapable 提供的类进行解析和模拟。
tapable 介绍
Webpack 本质上是一种事件流的机制,它的工作流程就是将各个插件串联起来,而实现这一切的核心就是 tapable ,Webpack 中最核心的,负责编译的 Compiler 和负责创建 bundles 的 Compilation 都是 tapable 构造函数的实例。
打开 Webpack 4.0 的源码中一定会看到下面这些以 Sync 、 Async 开头,以 Hook 结尾的方法,这些都是 tapable 核心库的类,为我们提供不同的事件流执行机制,我们称为 “钩子”。
// 引入 tapable 如下 const { SyncHook, SyncBailHook, SyncWaterfallHook, SyncLoopHook, AsyncParallelHook, AsyncParallelBailHook, AsyncSeriesHook, AsyncSeriesBailHook, AsyncSeriesWaterfallHook } = require("tapable");
上面的实现事件流机制的 “钩子” 大方向可以分为两个类别,“同步” 和 “异步”,“异步” 又分为两个类别,“并行” 和 “串行”,而 “同步” 的钩子都是串行的。
Sync 类型的钩子
1、SyncHook
SyncHook 为串行同步执行,不关心事件处理函数的返回值,在触发事件之后,会按照事件注册的先后顺序执行所有的事件处理函数。
// SyncHook 钩子的使用 const { SyncHook } = require("tapable"); // 创建实例 let syncHook = new SyncHook(["name", "age"]); // 注册事件 syncHook.tap("1", (name, age) => console.log("1", name, age)); syncHook.tap("2", (name, age) => console.log("2", name, age)); syncHook.tap("3", (name, age) => console.log("3", name, age)); // 触发事件,让监听函数执行 syncHook.call("panda", 18); // 1 panda 18 // 2 panda 18 // 3 panda 18
在 tapable 解构的 SyncHook 是一个类,注册事件需先创建实例,创建实例时支持传入一个数组,数组内存储事件触发时传入的参数,实例的 tap 方法用于注册事件,支持传入两个参数,第一个参数为事件名称,在 Webpack 中一般用于存储事件对应的插件名称(名字随意,只是起到注释作用), 第二个参数为事件处理函数,函数参数为执行 call 方法触发事件时所传入的参数的形参。
// 模拟 SyncHook 类 class SyncHook { constructor(args) { this.args = args; this.tasks = []; } tap(name, task) { this.tasks.push(task); } call(...args) { // 也可在参数不足时抛出异常 if (args.length < this.args.length) throw new Error("参数不足"); // 传入参数严格对应创建实例传入数组中的规定的参数,执行时多余的参数为 undefined args = args.slice(0, this.args.length); // 依次执行事件处理函数 this.tasks.forEach(task => task(...args)); } }
tasks 数组用于存储事件处理函数, call 方法调用时传入参数超过创建 SyncHook 实例传入的数组长度时,多余参数可处理为 undefined ,也可在参数不足时抛出异常,不灵活,后面的例子中就不再这样写了。
2、SyncBailHook
SyncBailHook 同样为串行同步执行,如果事件处理函数执行时有一个返回值不为空(即返回值为 undefined ),则跳过剩下未执行的事件处理函数(如类的名字,意义在于保险)。
// SyncBailHook 钩子的使用 const { SyncBailHook } = require("tapable"); // 创建实例 let 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("panda", 18); // 1 panda 18 // 2 panda 18
通过上面的用法可以看出, SyncHook 和 SyncBailHook 在逻辑上只是 call 方法不同,导致事件的执行机制不同,对于后面其他的 “钩子”,也是 call 的区别,接下来实现 SyncBailHook 类。
// 模拟 SyncBailHook 类 class SyncBailHook { constructor(args) { this.args = args; this.tasks = []; } tap(name, task) { this.tasks.push(task); } call(...args) { // 传入参数严格对应创建实例传入数组中的规定的参数,执行时多余的参数为 undefined args = args.slice(0, this.args.length); // 依次执行事件处理函数,如果返回值不为空,则停止向下执行 let i = 0, ret; do { ret = this.tasks[i++](...args); } while (!ret); } }