// 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 、 tapSync 、 tapPromise 和触发事件的方法 call 、 callAsync 、 promise 都是通过 compile 方法快速编译出来的,我们本文中这些方法的实现只是遵照了 tapable 库这些 “钩子” 的事件处理机制进行了模拟,以方便我们了解 tapable ,为学习 Webpack 原理做了一个铺垫,在 Webpack 中,这些 “钩子” 的真正作用就是将通过配置文件读取的插件与插件、加载器与加载器之间进行连接,“并行” 或 “串行” 执行,相信在我们对 tapable 中这些 “钩子” 的事件机制有所了解之后,再重新学习 Webpack 的源码应该会有所头绪。