webpack | 2020-06-13 16:01:20 705次 0次
wewpack 可以认为是一种基于事件流的编程范例,内部的工作流程都是基于 插件 机制串接起来,而将这些插件粘合起来的就是 webpack 自己写的基础类 Tapable,plugin 方法就是该类暴露出来的。tapable 是一个类似于 nodejs 的 EventEmitter 的库,主要是控制钩子函数的发布与订阅,控制着 webpack 的插件系。webpack 的本质就是一系列的插件运行。
本篇先介绍这个东西怎么来使用,为下一篇分析做准备,首先看下会用到的几种方法,分为同步方法和异步方法:
const { SyncHook, SyncBailHook, SyncWaterfallHook, SyncLoopHook, AsyncParallelHook, AsyncSeriesHook, AsyncSeriesWaterfallHook } = require("tapable");
一、SyncHook
/** * SyncHook 同步 hook 注册使用tap 执行使用call */ const syncHook = new SyncHook(["name", "age"]); // 注册事件 syncHook.tap("frist", (name, age) => { console.log("frist:", name, age); }); syncHook.tap("second", (name, age) => { console.log("second:", name, age); }); syncHook.tap("thrid", (name, age) => { console.log("thrid:", name, age); }); // 触发事件,让监听函数执行 syncHook.call("Mike", 18);
二、SyncBailHook
/** * syncBailHook * 上一个函数有返回值则终止后续的函数 */ const syncBailHook = new SyncBailHook(["name", "age"]); // 注册事件 syncBailHook.tap("frist", (name, age) => { console.log("frist:", name, age); }); syncBailHook.tap("second", (name, age) => { console.log("second:", name, age); return 'second'; }); syncBailHook.tap("thrid", (name, age) => { console.log("thrid:", name, age); }); // 触发事件,让监听函数执行 syncBailHook.call("Mike", 18);
三、SyncWaterfallHook
/** * syncWaterfallHook * 依赖上一个函数的返回值,最终调用可获取最后一个事件返回值 */ const syncWaterfallHook = new SyncWaterfallHook(["name", "age"]); // 注册事件 syncWaterfallHook.tap("1", (name, age) => { console.log("第一个函数事件名称", name, age); return 'frist data'; }); syncWaterfallHook.tap("2", (data) => { console.log("第二个函数接受上个函数值:", data); return 'second data'; }); syncWaterfallHook.tap("3", (data) => { console.log("第三个函数接受上个函数值:", data); return 'thrid data'; }); // 触发事件,让监听函数执行 const res = syncWaterfallHook.call("Mike", 18); console.log(res);
四、SyncLoopHook
可循环调用, 不返回 undefined 则循环执行,直到返回 undefined。
/** * syncLoopHook(webpack中未使用) * 可循环 不返回undefined 则循环执行,直到返回undefined 循环内部 do while循环 */ const 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("Mike", 18);
五、AsyncParallelHook
异步并行函数,可以通过回调函数调用和 promise 方式调用两种形式(此方法 webpack 中没有使用)。
方式一:tapAsync 注册 callAsync 执行
需要注意如果第一个先执行的回调中加了参数,最终的 callAsync 会立即执行,并且可以取到参数值,其他的 tapAsync 也会执行,但是执行完不会再进入最终回调,和书写顺序无关,而是看哪个先走完,传没传参数。
const asyncParallelHook = new AsyncParallelHook(["name", "age"]); // 注册事件 asyncParallelHook.tapAsync("1", (name, age, done) => { setTimeout(() => { console.log("1", name, age, new Date()); done(); //如果此时 done(1111) 加了参数 则此方法执行完立即进入callAsync方法 再进入下一个tapAsync执行 }, 1000); }); asyncParallelHook.tapAsync("2", (name, age, done) => { setTimeout(() => { console.log("2", name, age, new Date()); done(); /如果此时 done(1111) 加了参数 则此方法执行完立即进入callAsync方法 可以获取这个参数值 }, 2000); }); // 触发事件,让监听函数执行 asyncParallelHook.callAsync("Mike", 18, (res) => { console.log('函数执行完毕', res); });
方式二:tapPromise 注册 promise 执行
提示:之前可以在promise函数内部使用 resolve('1') 这样的,在外部使用 asyncParallelHook.promise("Mike", 18).then()。
但是现在不可以这样用了,内部 resolve 之后外部 then 无法捕获了。
const asyncParallelHook = new AsyncParallelHook(["name", "age"]); // peomise方式注册事件 asyncParallelHook.tapPromise("1", (name, age) => { return new Promise((resolve, reject) => { setTimeout(() => { console.log("1", name, age, new Date()); //resolve('第一次成功') 无效了 }, 1000); }); }); asyncParallelHook.tapPromise("2", (name, age) => { return new Promise((resolve, reject) => { setTimeout(() => { console.log("2", name, age, new Date()); //resolve('第二次成功') 无效了 }, 2000); }); }); // 触发事件,让监听函数执行 asyncParallelHook.promise("Mike", 18)
六、AsyncSeriesHook
异步串行执行,具体原理实现可以通过 reduce 函数借助,类似 redux 中间件实现方式。
方式一:tapAsync 注册 callAsync 执行
需要注意如果第一个先执行的回调中加了参数,最终的 callAsync 会立即执行,并且可以取到参数值,其他的 tapAsync 则不会继续执行,注意和上面的方法区别,因为这是串行函数,和书写顺序无关,而是看哪个先走完,传没传参数。
const asyncSeriesHook = new AsyncSeriesHook(["name", "age"]); // 注册事件 asyncSeriesHook.tapAsync("1", (name, age, done) => { setTimeout(() => { console.log("1", name, age, new Date()); done(); }, 1000); }); asyncSeriesHook.tapAsync("2", (name, age, done) => { setTimeout(() => { console.log("2", name, age, new Date()); done(); }, 2000); }); // 触发事件,让监听函数执行 asyncSeriesHook.callAsync("Mike", 18, () => { console.log('执行完成'); });
方式二:tapPromise 注册 promise 执行
asyncSeriesHook 和 asyncParallelHook 相比 promise 的写法有差异:
1. 必须 resolve 才可以进入下一个tap注册函数
2. 触发事件可以执行 then 方法,但是无法获取 reslove 中的值
const asyncSeriesHook = new AsyncSeriesHook(["name", "age"]); // 注册事件 asyncSeriesHook.tapPromise("1", (name, age) => { return new Promise((resolve, reject) => { setTimeout(() => { console.log("1", name, age, new Date()); resolve(111); }, 1000); }) }); asyncSeriesHook.tapPromise("2", (name, age) => { return new Promise((resolve, reject) => { setTimeout(() => { console.log("2", name, age, new Date()); resolve(222); }, 2000); }); }); // 触发事件,让监听函数执行 asyncSeriesHook.promise("Mike", 18).then(res => { console.log('执行完毕,但是res是获取不到的') });
七、AsyncSeriesWaterfallHook
异步串行阻塞执行。
方式一:tapAsync 注册 callAsync 执行
需要注意如果第一个先执行的回调中加了参数,并且第一个参数不为 null 最终的 callAsync 会立即执行,并且可以取到参数值,其他的 tapAsync 则不会继续执行,如果为 null 则不会阻断,最后一个执行的 tapAsync 中第一个参数的值会给到最终调用的 callAsync 中。
const asyncSeriesWaterfallHook = new AsyncSeriesWaterfallHook(["name", "age"]); // 注册事件 asyncSeriesWaterfallHook.tapAsync("1", (name, age, done) => { setTimeout(() => { console.log("1", name, age, new Date()); // done(null, 11111); 第一个参数为null则可以继续 done('no error', 11111); // 第一个参数 err 有值则会阻塞后面所有执行 直到执行的那个回调中,将错误信息给出 }, 1000); }); asyncSeriesWaterfallHook.tapAsync("2", (data, age, done) => { setTimeout(() => { console.log("2", data, age, new Date()); done(2222222222); //最后一个的这个第一个参数设不设置err无用,会返回给最终回调 }, 2000); }); // 触发事件,让监听函数执行 asyncSeriesWaterfallHook.callAsync("Mike", 18, (data) => { console.log('执行完成', data); });
方式二:tapPromise 注册 promise 执行
const asyncSeriesWaterfallHook = new AsyncSeriesWaterfallHook(["name", "age"]); // 注册事件 asyncSeriesWaterfallHook.tapPromise("1", (name, age) => { return new Promise((resolve, reject) => { setTimeout(() => { console.log("1", name, age, new Date()); // reject(111); 抛出错误 resolve(1111) }, 1000); }) }); asyncSeriesWaterfallHook.tapPromise("2", (name, age) => { return new Promise((resolve, reject) => { setTimeout(() => { //如果上个中resolve值,这里只会取到resolve中传的第一个值 age不会受影响 console.log("2", name, age, new Date()); resolve(222); }, 2000); }); }); // 触发事件,让监听函数执行 asyncSeriesWaterfallHook.promise("Mike", 18).then(res => { console.log('执行完毕', res) }).catch(err => { console.log('结束有错误:', err) })
八、拦截器 & context
所有钩子都提供额外的拦截器 API:
① call:实例方法 call 或者 callAsync 之前调用,即使多个 tap 注册此钩子只会执行一次
② register:每次注册 tap 事件触发后就会触发这个,会被触发多次
③ loop:循环钩子(LoopHook, 就是类型 Loop)触发
④ tap:实例方法 call 或者 callAsync 调用之后,tap 注册函数执行之前此方法被调用
const h1 = new SyncHook(['xxx']); h1.tap('A', function(args) { console.log('a', args); }); h1.tap({ name: 'B', context: true }, function() { console.log('d'); }); h1.intercept({ //call之前调用 call: (...args) => { console.log(...args, 'call 方法被执行了~~~~~~'); }, //每次注册 tap 事件触发后就会触发这个 register: (tap) => { console.log(tap, '222222'); return tap; }, loop: (...args) => { console.log(...args, '33333'); }, //call执行后 先执行这个 再执行 tap 中注册的函数 tap: (tap) => { console.log(tap, '444444'); } }); h1.tap('C', function() { console.log('e'); }); h1.call("Mike");
其中在 tap 注册时候可以传入 context 为 true,这样 tap 注册器中或者 intercept 拦截器中各个钩子的第一个参数则可以设置这个 context 的值,全局使用。context 在实现原理上就是一个全局对象,各个方法执行时先判断是否指定了 context 为 true,然后将这个全局对象放在回调中第一个参数上。
本篇介绍了 tapable 中一些方法的使用,下一篇继续进入源码分析。
0人赞