Webpack 是一个现代 JavaScript 应用程序的静态模块打包器,是对前端项目实现自动化和优化必不可少的工具,Webpack 的 loader
(加载器)和 plugin
(插件)是由 Webpack 开发者和社区开发者共同贡献的,而目前又没有比较系统的开发文档,想写加载器和插件必需要懂 Webpack 的原理,即看懂 Webpack 的源码,tapable
则是 Webpack 依赖的核心库,能够说不懂 tapable
就看不懂 Webpack 源码,因此本篇会对 tapable
提供的类进行解析和模拟。前端
Webpack 本质上是一种事件流的机制,它的工做流程就是将各个插件串联起来,而实现这一切的核心就是 tapable
,Webpack 中最核心的,负责编译的 Compiler
和负责建立 bundles
的 Compilation
都是 tapable
构造函数的实例。数组
打开 Webpack 4.0
的源码中必定会看到下面这些以 Sync
、Async
开头,以 Hook
结尾的方法,这些都是 tapable
核心库的类,为咱们提供不一样的事件流执行机制,咱们称为 “钩子”。promise
// 引入 tapable 以下 const { SyncHook, SyncBailHook, SyncWaterfallHook, SyncLoopHook, AsyncParallelHook, AsyncParallelBailHook, AsyncSeriesHook, AsyncSeriesBailHook, AsyncSeriesWaterfallHook } = require("tapable");
上面的实现事件流机制的 “钩子” 大方向能够分为两个类别,“同步” 和 “异步”,“异步” 又分为两个类别,“并行” 和 “串行”,而 “同步” 的钩子都是串行的。并发
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
方法触发事件时所传入的参数的形参。async
// 模拟 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
,也可在参数不足时抛出异常,不灵活,后面的例子中就再也不这样写了。函数
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
类。oop
// 模拟 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); } }
在上面代码的 call
方法中,咱们设置返回值为 ret
,第一次执行后没有返回值则继续循环执行,若是有返回值则当即中止循环,即实现 “保险” 的功能。学习
SyncWaterfallHook
为串行同步执行,上一个事件处理函数的返回值做为参数传递给下一个事件处理函数,依次类推,正因如此,只有第一个事件处理函数的参数能够经过 call
传递,而 call
的返回值为最后一个事件处理函数的返回值。
// SyncWaterfallHook 钩子的使用 const { SyncWaterfallHook } = require("tapable"); // 建立实例 let syncWaterfallHook = new SyncWaterfallHook(["name", "age"]); // 注册事件 syncWaterfallHook.tap("1", (name, age) => { console.log("1", name, age); return "1"; }); syncWaterfallHook.tap("2", data => { console.log("2", data); return "2"; }); syncWaterfallHook.tap("3", data => { console.log("3", data); return "3" }); // 触发事件,让监听函数执行 let ret = syncWaterfallHook.call("panda", 18); console.log("call", ret); // 1 panda 18 // 2 1 // 3 2 // call 3
SyncWaterfallHook
名称中含有 “瀑布”,经过上面代码能够看出 “瀑布” 形象生动的描绘了事件处理函数执行的特色,与 SyncHook
和 SyncBailHook
的区别就在于事件处理函数返回结果的流动性,接下来看一下 SyncWaterfallHook
类的实现。
// 模拟 SyncWaterfallHook 类 class SyncWaterfallHook { 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 [first, ...others] = this.tasks; return reduce((ret, task) => task(ret), first(...args)); } }
上面代码中 call
的逻辑是将存储事件处理函数的 tasks
拆成两部分,分别为第一个事件处理函数,和存储其他事件处理函数的数组,使用 reduce
进行归并,将第一个事件处理函数执行后的返回值做为归并的初始值,依次调用其他事件处理函数并传递上一次归并的返回值。
SyncLoopHook
为串行同步执行,事件处理函数返回 true
表示继续循环,即循环执行当前事件处理函数,返回 undefined
表示结束循环,SyncLoopHook
与 SyncBailHook
的循环不一样,SyncBailHook
只决定是否继续向下执行后面的事件处理函数,而 SyncLoopHook
的循环是指循环执行每个事件处理函数,直到返回 undefined
为止,才会继续向下执行其余事件处理函数,执行机制同理。
// SyncLoopHook 钩子的使用 const { SyncLoopHook } = require("tapable"); // 建立实例 let syncLoopHook = new SyncLoopHook(["name", "age"]); // 定义辅助变量 let total1 = 0; let total2 = 0; // 注册事件 syncLoopHook.tap("1", (name, age) => { console.log("1", name, age, total1); return total1++ < 2 ? true : undefined; }); syncLoopHook.tap("2", (name, age) => { console.log("2", name, age, total2); return total2++ < 2 ? true : undefined; }); syncLoopHook.tap("3", (name, age) => console.log("3", name, age)); // 触发事件,让监听函数执行 syncLoopHook.call("panda", 18); // 1 panda 18 0 // 1 panda 18 1 // 1 panda 18 2 // 2 panda 18 0 // 2 panda 18 1 // 2 panda 18 2 // 3 panda 18
经过上面的执行结果能够清楚的看到 SyncLoopHook
的执行机制,但有一点须要注意,返回值必须严格是 true
才会触发循环,屡次执行当前事件处理函数,必须严格返回 undefined
,才会结束循环,去执行后面的事件处理函数,若是事件处理函数的返回值不是 true
也不是 undefined
,则会死循环。
在了解 SyncLoopHook
的执行机制之后,咱们接下来看看 SyncLoopHook
的 call
方法是如何实现的。
// 模拟 SyncLoopHook 类 class SyncLoopHook { constructor(args) { this.args = args; this.tasks = []; } tap(name, task) { this.tasks.push(task); } call(...args) { // 传入参数严格对应建立实例传入数组中的规定的参数,执行时多余的参数为 undefined args = args.slice(0, this.args.length); // 依次执行事件处理函数,若是返回值为 true,则继续执行当前事件处理函数 // 直到返回 undefined,则继续向下执行其余事件处理函数 this.tasks.forEach(task => { let ret; do { ret = this.task(...args); } while (ret === true || !(ret === undefined)); }); } }
在上面代码中能够看到 SyncLoopHook
类 call
方法的实现更像是 SyncHook
和 SyncBailHook
的 call
方法的结合版,外层循环整个 tasks
事件处理函数队列,内层经过返回值进行循环,控制每个事件处理函数的执行次数。
注意:在 Sync 类型 “钩子” 下执行的插件都是顺序执行的,只能使用 tab
注册。
Async
类型可使用 tap
、tapAsync
和 tapPromise
注册不一样类型的插件 “钩子”,分别经过 call
、callAsync
和 promise
方法调用,咱们下面会针对 AsyncParallelHook
和 AsyncSeriesHook
的 async
和 promise
两种方式分别介绍和模拟。
AsyncParallelHook
为异步并行执行,经过 tapAsync
注册的事件,经过 callAsync
触发,经过 tapPromise
注册的事件,经过 promise
触发(返回值能够调用 then
方法)。
callAsync
的最后一个参数为回调函数,在全部事件处理函数执行完毕后执行。
// AsyncParallelHook 钩子:tapAsync/callAsync 的使用 const { AsyncParallelHook } = require("tapable"); // 建立实例 let asyncParallelHook = new AsyncParallelHook(["name", "age"]); // 注册事件 console.time("time"); asyncParallelHook.tapAsync("1", (name, age, done) => { settimeout(() => { console.log("1", name, age, new Date()); done(); }, 1000); }); asyncParallelHook.tapAsync("2", (name, age, done) => { settimeout(() => { console.log("2", name, age, new Date()); done(); }, 2000); }); asyncParallelHook.tapAsync("3", (name, age, done) => { settimeout(() => { console.log("3", name, age, new Date()); done(); console.timeEnd("time"); }, 3000); }); // 触发事件,让监听函数执行 asyncParallelHook.callAsync("panda", 18, () => { console.log("complete"); }); // 1 panda 18 2018-08-07T10:38:32.675Z // 2 panda 18 2018-08-07T10:38:33.674Z // 3 panda 18 2018-08-07T10:38:34.674Z // complete // time: 3005.060ms
异步并行是指,事件处理函数内三个定时器的异步操做最长时间为 3s
,而三个事件处理函数执行完成总共用时接近 3s
,因此三个事件处理函数是几乎同时执行的,不需等待。
全部 tabAsync
注册的事件处理函数最后一个参数都为一个回调函数 done
,每一个事件处理函数在异步代码执行完毕后调用 done
函数,则能够保证 callAsync
会在全部异步函数都执行完毕后执行,接下来看一看 callAsync
是如何实现的。
// 模拟 AsyncParallelHook 类:tapAsync/callAsync class AsyncParallelHook { constructor(args) { this.args = args; this.tasks = []; } tabAsync(name, task) { this.tasks.push(task); } callAsync(...args) { // 先取出最后传入的回调函数 let finalCallback = args.pop(); // 传入参数严格对应建立实例传入数组中的规定的参数,执行时多余的参数为 undefined args = args.slice(0, this.args.length); // 定义一个 i 变量和 done 函数,每次执行检测 i 值和队列长度,决定是否执行 callAsync 的回调函数 let i = 0; let done = () => { if (++i === this.tasks.length) { finalCallback(); } }; // 依次执行事件处理函数 this.tasks.forEach(task => task(...args, done)); } }
在 callAsync
中,将最后一个参数(全部事件处理函数执行完毕后执行的回调)取出,并定义 done
函数,经过比较 i
和存储事件处理函数的数组 tasks
的 length
来肯定回调是否执行,循环执行每个事件处理函数并将 done
做为最后一个参数传入,因此每一个事件处理函数内部的异步操做完成时,执行 done
就是为了检测是否是该执行 callAsync
的回调,当全部事件处理函数均执行完毕知足 done
函数内部 i
和 length
相等的条件时,则调用 callAsync
的回调。
要使用 tapPromise
注册事件,对事件处理函数有一个要求,必须返回一个 Promise 实例,而 promise
方法也返回一个 Promise 实例,callAsync
的回调函数在 promise
方法中用 then
的方式代替。
// AsyncParallelHook 钩子:tapPromise/promise 的使用 const { AsyncParallelHook } = require("tapable"); // 建立实例 let asyncParallelHook = new AsyncParallelHook(["name", "age"]); // 注册事件 console.time("time"); asyncParallelHook.tapPromise("1", (name, age) => { return new Promise((resolve, reject) => { settimeout(() => { console.log("1", name, age, new Date()); resolve("1"); }, 1000); }); }); asyncParallelHook.tapPromise("2", (name, age) => { return new Promise((resolve, reject) => { settimeout(() => { console.log("2", name, age, new Date()); resolve("2"); }, 2000); }); }); asyncParallelHook.tapPromise("3", (name, age) => { return new Promise((resolve, reject) => { settimeout(() => { console.log("3", name, age, new Date()); resolve("3"); console.timeEnd("time"); }, 3000); }); }); // 触发事件,让监听函数执行 asyncParallelHook.promise("panda", 18).then(ret => { console.log(ret); }); // 1 panda 18 2018-08-07T12:17:21.741Z // 2 panda 18 2018-08-07T12:17:22.736Z // 3 panda 18 2018-08-07T12:17:23.739Z // time: 3006.542ms // [ '1', '2', '3' ]
上面每个 tapPromise
注册事件的事件处理函数都返回一个 Promise 实例,并将返回值传入 resolve
方法,调用 promise
方法触发事件时,若是全部事件处理函数返回的 Promise 实例结果都成功,会将结果存储在数组中,并做为参数传递给 promise
的 then
方法中成功的回调,若是有一个失败就是将失败的结果返回做为参数传递给失败的回调。
// 模拟 AsyncParallelHook 类 tapPromise/promise class AsyncParallelHook { constructor(args) { this.args = args; this.tasks = []; } tapPromise(name, task) { this.tasks.push(task); } promise(...args) { // 传入参数严格对应建立实例传入数组中的规定的参数,执行时多余的参数为 undefined args = args.slice(0, this.args.length); // 将全部事件处理函数转换成 Promise 实例,并发执行全部的 Promise return Promise.all(this.tasks.map(task => task(...args))); } }
其实根据上面对于 tapPromise
和 promise
使用的描述就能够猜到,promise
方法的逻辑是经过 Promise.all
来实现的。
AsyncSeriesHook
为异步串行执行,与 AsyncParallelHook
相同,经过 tapAsync
注册的事件,经过 callAsync
触发,经过 tapPromise
注册的事件,经过 promise
触发,能够调用 then
方法。
与 AsyncParallelHook
的 callAsync
方法相似,AsyncSeriesHook
的 callAsync
方法也是经过传入回调函数的方式,在全部事件处理函数执行完毕后执行 callAsync
的回调函数。
// AsyncSeriesHook 钩子:tapAsync/callAsync 的使用 const { AsyncSeriesHook } = require("tapable"); // 建立实例 let asyncSeriesHook = new AsyncSeriesHook(["name", "age"]); // 注册事件 console.time("time"); asyncSeriesHook.tapAsync("1", (name, age, next) => { settimeout(() => { console.log("1", name, age, new Date()); next(); }, 1000); }); asyncSeriesHook.tapAsync("2", (name, age, next) => { settimeout(() => { console.log("2", name, age, new Date()); next(); }, 2000); }); asyncSeriesHook.tapAsync("3", (name, age, next) => { settimeout(() => { console.log("3", name, age, new Date()); next(); console.timeEnd("time"); }, 3000); }); // 触发事件,让监听函数执行 asyncSeriesHook.callAsync("panda", 18, () => { console.log("complete"); }); // 1 panda 18 2018-08-07T14:40:52.896Z // 2 panda 18 2018-08-07T14:40:54.901Z // 3 panda 18 2018-08-07T14:40:57.901Z // complete // time: 6008.790ms
异步串行是指,事件处理函数内三个定时器的异步执行时间分别为 1s
、2s
和 3s
,而三个事件处理函数执行完总共用时接近 6s
,因此三个事件处理函数执行是须要排队的,必须一个一个执行,当前事件处理函数执行完才能执行下一个。
AsyncSeriesHook
类的 tabAsync
方法注册的事件处理函数参数中的 next
能够与 AsyncParallelHook
类中 tabAsync
方法参数的 done
进行类比,同为回调函数,不一样点在于 AsyncSeriesHook
与 AsyncParallelHook
的 callAsync
方法的 “并行” 和 “串行” 的实现方式。
// 模拟 AsyncSeriesHook 类:tapAsync/callAsync class AsyncSeriesHook { constructor(args) { this.args = args; this.tasks = []; } tabAsync(name, task) { this.tasks.push(task); } callAsync(...args) { // 先取出最后传入的回调函数 let finalCallback = args.pop(); // 传入参数严格对应建立实例传入数组中的规定的参数,执行时多余的参数为 undefined args = args.slice(0, this.args.length); // 定义一个 i 变量和 next 函数,每次取出一个事件处理函数执行,并维护 i 的值 // 直到全部事件处理函数都执行完,调用 callAsync 的回调 // 若是事件处理函数中没有调用 next,则没法继续 let i = 0; let next = () => { let task = this.tasks[i++]; task ? task(...args, next) : finalCallback(); }; next(); } }
AsyncParallelHook
是经过循环依次执行了全部的事件处理函数,done
方法只为了检测是否已经知足条件执行 callAsync
的回调,若是中间某个事件处理函数没有调用 done
,只是不会调用 callAsync
的回调,可是全部的事件处理函数都执行了。
而 AsyncSeriesHook
的 next
执行机制更像 Express
和 Koa
中的中间件,在注册事件的回调中若是不调用 next
,则在触发事件时会在没有调用 next
的事件处理函数的位置 “卡死”,即不会继续执行后面的事件处理函数,只有都调用 next
才能继续,而最后一个事件处理函数中调用 next
决定是否调用 callAsync
的回调。
与 AsyncParallelHook
相似,tapPromise
注册事件的事件处理函数须要返回一个 Promise 实例,promise
方法最后也返回一个 Promise 实例。
// AsyncSeriesHook 钩子:tapPromise/promise 的使用 const { AsyncSeriesHook } = require("tapable"); // 建立实例 let asyncSeriesHook = new AsyncSeriesHook(["name", "age"]); // 注册事件 console.time("time"); asyncSeriesHook.tapPromise("1", (name, age) => { return new Promise((resolve, reject) => { settimeout(() => { console.log("1", name, age, new Date()); resolve("1"); }, 1000); }); }); asyncSeriesHook.tapPromise("2", (name, age) => { return new Promise((resolve, reject) => { settimeout(() => { console.log("2", name, age, new Date()); resolve("2"); }, 2000); }); }); asyncParallelHook.tapPromise("3", (name, age) => { return new Promise((resolve, reject) => { settimeout(() => { console.log("3", name, age, new Date()); resolve("3"); console.timeEnd("time"); }, 3000); }); }); // 触发事件,让监听函数执行 asyncSeriesHook.promise("panda", 18).then(ret => { console.log(ret); }); // 1 panda 18 2018-08-07T14:45:52.896Z // 2 panda 18 2018-08-07T14:45:54.901Z // 3 panda 18 2018-08-07T14:45:57.901Z // time: 6014.291ms // [ '1', '2', '3' ]
分析上面的执行过程,全部的事件处理函数都返回了 Promise 的实例,若是想实现 “串行”,则须要让每个返回的 Promise 实例都调用 then
,并在 then
中执行下一个事件处理函数,这样就保证了只有上一个事件处理函数执行完后才会执行下一个。
// 模拟 AsyncSeriesHook 类 tapPromise/promise class AsyncSeriesHook { constructor(args) { this.args = args; this.tasks = []; } tapPromise(name, task) { this.tasks.push(task); } promise(...args) { // 传入参数严格对应建立实例传入数组中的规定的参数,执行时多余的参数为 undefined args = args.slice(0, this.args.length); // 将每一个事件处理函数执行并调用返回 Promise 实例的 then 方法 // 让下一个事件处理函数在 then 方法成功的回调中执行 let [first, ...others] = this.tasks; return others.reduce((promise, task) => { return promise.then(() => task(...args)); }, first(...args)); } }
上面代码中的 “串行” 是使用 reduce
归并来实现的,首先将存储全部事件处理函数的数组 tasks
解构成两部分,第一个事件处理函数和存储其余事件处理函数的数组 others
,对 others
进行归并,将第一个事件处理函数执行后返回的 Promise 实例做为归并的初始值,这样在归并的过程当中上一个值始终是上一个事件处理函数返回的 Promise 实例,能够直接调用 then
方法,并在 then
的回调中执行下一个事件处理函数,直到归并完成,将 reduce
最后返回的 Promise 实例做为 promise
方法的返回值,则实现 promise
方法执行后继续调用 then
来实现后续逻辑。
在上面 Async
异步类型的 “钩子中”,咱们只着重介绍了 “串行” 和 “并行”(AsyncParallelHook
和 AsyncSeriesHook
)以及回调和 Promise 的两种注册和触发事件的方式,还有一些其余的具备必定特色的异步 “钩子” 咱们并无进行分析,由于他们的机制与同步对应的 “钩子” 很是的类似。
AsyncParallelBailHook
和 AsyncSeriesBailHook
分别为异步 “并行” 和 “串行” 执行的 “钩子”,返回值不为 undefined
,即有返回值,则当即中止向下执行其余事件处理函数,实现逻辑可结合 AsyncParallelHook
、AsyncSeriesHook
和 SyncBailHook
。
AsyncSeriesWaterfallHook
为异步 “串行” 执行的 “钩子”,上一个事件处理函数的返回值做为参数传递给下一个事件处理函数,实现逻辑可结合 AsyncSeriesHook
和 SyncWaterfallHook
。
在 tapable
源码中,注册事件的方法 tab
、tapAsync
、tapPromise
和触发事件的方法 call
、callAsync
、promise
都是经过 compile
方法快速编译出来的,咱们本文中这些方法的实现只是遵守了 tapable
库这些 “钩子” 的事件处理机制进行了模拟,以方便咱们了解 tapable
,为学习 Webpack 原理作了一个铺垫,在 Webpack 中,这些 “钩子” 的真正做用就是将经过配置文件读取的插件与插件、加载器与加载器之间进行链接,“并行” 或 “串行” 执行,相信在咱们对 tapable
中这些 “钩子” 的事件机制有所了解以后,再从新学习 Webpack 的源码应该会有所头绪。