最近在看webpack
的源码,发现有个比较头疼的点是:代码看起来很是跳跃,每每看不到几行就插入一段新内容,为了理解又不得不先学习相关的前置知识。层层嵌套以后,发现最基础的仍是tapable
模型,所以先对这部分的内容作一个介绍。前端
Webpack
的流程能够分为如下三大阶段:node
初始化:启动构建,读取与合并配置参数,加载 Plugin
,实例化 Compiler。这个compile对象会穿行在本次编译的整个周期。
编译:从 Entry 发出,针对每一个 Module 串行调用对应的 Loader 去翻译文件内容,再找到该 Module 依赖的 Module,递归地进行编译处理。
输出:对编译后的 Module
组合成 Chunk
,把 Chunk
转换成文件,输出到文件系统。webpack
在这个过程当中,最核心的就是插件化的设计: 在不一样的阶段执行相应的一些插件,来执行某些功能。
而这里的阶段,指的就是hook
。 理论太抽象,来看一段webpack的源码(4.x版本):web
// webpack/lib/MultiCompiler.js const { Tapable, SyncHook, MultiHook } = require("tapable"); class MultiCompiler extends Tapable { constructor(compilers) { super(); this.hooks = { done: new SyncHook(["stats"]), invalid: new MultiHook(compilers.map(c => c.hooks.invalid)), run: new MultiHook(compilers.map(c => c.hooks.run)), watchClose: new SyncHook([]), watchRun: new MultiHook(compilers.map(c => c.hooks.watchRun)), infrastructureLog: new MultiHook( compilers.map(c => c.hooks.infrastructureLog) ) }; } /// 省略其余代码 }
这是compile
的构造函数,有几个注意点:数组
Tapable
,也就是本文的话题对象done
invalid
, run
, watchClose
等等都是内置的生命周期,具体的代码暂时不去关心。这部分代码主要是为了说明一个思路: webpack 的生命周期hook
,其实是一个个插件的集合,表明的含义是,在某个阶段须要挂载某些插件。
到这里,脑海里有这种大概雏形就好,接下来咱们开始介绍Tapable
。promise
Tapable的核心思路有点相似于nodejs
中的events
,最基本的发布/订阅模式。并发
const EventEmitter = require('events'); const myEmitter = new EventEmitter(); // 注册事件对应的监听函数 myEmitter.on('安歌发布新文章', (title, tag) => { console.log("前去围观并吐槽",title, tag) }); // 触发事件 并传入参数 myEmitter.emit('安歌发布新文章',’标题tapable机制‘, '标签webpack');
这个结构很简单也很清晰:异步
tapable
的核心用法与此类似,那为何屡次一举要使用它呢?async
根据前面的demo,不妨假设一下,若是咱们注册了不少事件,好比event.on(’起床‘)
,event.on(’吃饭‘)
,event.on(’上班‘)
等等,那事件之间可能就存在一些依赖关系,好比要先起床而后才能上班这样的时序依赖,而tapable
就能够帮助咱们很方便的管理这些关系。函数
接下来用一个前几天参加的公司中秋晚会的例子,来简单说明一下Tapable
的用法:
我把本身的参加流程分红如下阶段:
晚宴前
晚宴中
晚宴后
那么先写个全局demo:
// 1. 引入 tapable ,先无论具体的钩子类型 const { SyncHook, SyncBailHook, SyncWaterfallHook, } = require("tapable"); // 2. 定义不一样阶段对应的钩子, // 钩子: 晚宴前 let beforeDinner = new SyncHook(["stageName"]); // 钩子:晚宴中 let atTheDinner = new SyncBailHook(["stageName"]); // 钩子 晚宴后 let afterDinner = new SyncWaterfallHook(["stageName"]); // 3. 为不一样阶段注册事件,这里先写出晚宴前的事件 beforeDinner.tap('检查着装', (stageName)=>{ console.log(`${stageName}: 检查着装`) }) beforeDinner.tap('乘坐班车到酒店', (stageName)=>{ console.log(`${stageName}: 乘坐班车到酒店`) }) // 每一个阶段触发自身须要执行的事件 beforeDinner.call('晚宴前'); atTheDinner.call('晚宴中'); afterDinner.call('晚宴后'); // 输出结果: // 晚宴前: 检查着装 // 晚宴前: 乘坐班车到酒店 // ... 省略后面的输出
这个demo
简单的定义了三个阶段,先不去关具体的hook
类型,了解下总体的结构:
beforeDinner
在实例化时,使用数组声明了参数stageName
, 这个地方的参数类型仅仅做为接口定义的目的使用,为了方便触发的时候传入对应的参数;call
方法其实就相似前文的emit
,与之不一样的是,event.emit
表示事件触发,而hook.call
表示当前钩子要开始执行钩子上注册的全部事件。(固然咱们只注册了晚宴前的2个事件)hook.call(param)
执行以后,该hook
对应的事件就按照注册顺序以及特定规则(具体规则后面说明,暂时略过)依次执行,所以上面的beforeDinner.call('晚宴前');
会输出对应的阶段名称和事件名称。到这里,咱们已经用上了最基本的tapable
了。回顾下它和events
最大的区别:
tapable
不只提供了事件的注册和执行,还用不一样的Hook
将事件进行分类(这里例子用三个阶段将基础事件分类)
SyncBailHook
接下来就是晚宴中的事件,这里有个注意点:晚宴中的第三个事件”若是成为当前桌状元,那么就留下来博王中王“是一个带有前提条件的事件,因此咱们用了SyncBailHook
,而且这么注册事件:
atTheDinner.tap('用餐并欣赏表演', (stageName) => { console.log(`${stageName}: 用餐并欣赏表演`); }) atTheDinner.tap('在当前桌进行博饼', (stageName) => { console.log(`${stageName}: 在当前桌进行博饼`); // 关键伪代码 let getChampion = false //若是得到状元 if(!getChampion){ console.log(`${stageName}: 没有得到当前桌状元,不须要参与博王中王`); // 注意这里的return return '提早结束!'; } }) atTheDinner.tap('博王中王', (stageName) => { console.log(`${stageName}: 博王中王`); })
SyncBailHook翻译过来意思是“熔断类型的钩子”,做用就像保险丝,一旦有危险,则启动保护(一旦该钩子的某个事件,执行返回除了undefined之外的值,后面注册的事件就再也不执行)。正如前面的例子中,若是在“当前桌子博饼”中没有成功搏到“状元”,就不会进行后面的“搏王中王”事件。经常使用于处理某些须要条件判断才触发的事件。
SyncWaterfallHook
晚宴以后的事件,与前面不用的地方在于:事件2发朋友圈 用的是事件1中所拍的照片,换句话说后面的事件依赖于前面事件的执行结果。因此能够这么写:
afterDinner.tap('回家前拍照', (stageName) => { console.log(`${stageName}: 拍一些照片,打车回家`); let pictures = ['image1','image2']; return pictures; }) afterDinner.tap('回家后发朋友圈', (pictures)=> { // 注意这里的内置参数 再也不是stageName 而是pictures return console.log(`回家后,用${pictures}:发朋友圈`); })
实例化afterDinner
时使用了SyncWaterfallHook
,顾名思义,这种瀑布式的钩子,做用就是在执行该钩子内注册的事件时,会把每一个阶段的执行结果传递给后面的阶段。
这部分咱们介绍了tapable
的基本用法和三种基本类型的hook
,大概能够总结一下:
hook
表示事件的集合,hook
的类型决定了注册在这个hook
的事件如何执行
hook
开胃菜结束,接下来要真正开始系统化的了解tapable
了,(好消息是若是前面的例子都看懂了,后面的学起来会很是简单,坏消息是:又要涉及前端最棘手的问题之一--异步)
先来一览全部的hook
类型:
整体上,hook
类型分红同步和异步两大类,异步再分为异步串行和异步并行。
先前已经介绍了同步hook
里面的前三种。第四种SynloopHook
也简要介绍下:
假设写文章这个事情,分红校对和发表两个步骤,校对必须3次以上,才能够执行发表事件:
// 当监听函数被触发的时候,若是该监听函数返回true时则这个监听函数会反复执行,若是返回 undefined 则表示退出循环 let writeArticle = SyncLoopHook(); let count = 0; writeArticle.tap('校对',()=>{ console.log('执行校对', count++) if(count<3){ return true; // 没有达到3次则继续校对 } return }) writeArticle.tap('发表',()=>{ console.log('发表') })
AsyncParallelHook
异步的hook,注册和触发能够用tapAsync/callAsync
和tapPromise/promise
两种语法,写法上略有不用。直接上demo:
// AsyncParallelHook 钩子:tapAsync/callAsync 的使用 const { AsyncParallelHook } = require("tapable"); // 建立实例 let asyncParallelHook = new AsyncParallelHook(["demoName"]); // 注册事件 console.time("time"); asyncParallelHook.tapAsync("异步事件1", (demoName, done) => { setTimeout(() => { console.log("1", demoName, new Date()); done(); //须要注意的是这里的`done`方法 }, 1000); }); asyncParallelHook.tapAsync("异步事件2", (demoName, done) => { setTimeout(() => { console.log("2", demoName, new Date()); done(); }, 2000); }); asyncParallelHook.tapAsync("异步事件3", (demoName, done) => { setTimeout(() => { console.log("3", demoName, new Date()); done(); console.timeEnd("time"); }, 3000); }); // 触发事件,让监听函数执行 asyncParallelHook.callAsync("异步并行", () => { // 只有当前钩子的全部事件都执行done 才进入这个callback console.log("complete"); }); // 输出 // 异步事件1 异步并行 // Sun Sep 08 2019 21:24:12 GMT+0800 (GMT+08:00) {} // 异步事件2 异步并行 // Sun Sep 08 2019 21:24:13 GMT+0800 (GMT+08:00) {} // 异步事件3 异步并行 // Sun Sep 08 2019 21:24:14 GMT+0800 (GMT+08:00) {} // complete // time: 3007.266845703125ms // time: 3007.640ms
须要注意的是这里的done
方法, 每一个注册的的事件均可以调用到这个done
方法,这个方法的做用是:向对应的hook实例告知,当前的异步事件完成,只有当全部的事件回调都执行了done
方法,才会进入钩子自己的回调函数(demo中的console.log("complete");)
从例子中的计时状况来看,很明显全部的事件是并行的 -- 事件1 2 3分别须要1s 2s 3s, 最终执行完也只花了3s。
用tapPromise/promise
来写的话,以下:
asyncParallelHook.tapPromise("异步事件1", (demoName) => { return new Promise((resolve, reject) => { setTimeout(() => { console.log("1", demoName, new Date()); resolve("1"); }, 1000); }); }); // ...省略重复代码 asyncParallelHook.promise("异步并行").then(() => { console.log("最终结果", new Date()); }).catch(err => { console.log("发现错误", new Date()); });
区别在于:
tapPromise
注册时,回调函数必须返回一个promise
tabAsync
注册使用done
表示当前执行完成,使用tapPromise
时则只要使用resolve()
便可resolve
, 而是reject(error)
,那么会进入asyncParallelHook
的catch
而不是then
这种写法其实很相似ES6中的promise.all
,比较好理解
AsyncSeriesHook
其实到这里,已经一只脚踏进成功的大门了。 异步串行和异步并行的写法,彻底同样。只须要简单把前面例子中,实例化的语句改为:
let asyncSeriesHook = new AsyncSeriesHook()
而后看看3个异步事件执行完后的事件间隔(并行的时候是3s,串行时总时长变成6s)。
没错,就是这么简单~!
webpack-dev-middleware
中tapable
的应用webpack-dev-middleware
是一个webpack的插件,做用是监听webpack的编译变化并写入到内存中。 核心代码:
// webpack-dev-middleware/lib/context.js const context = { state: false, webpackStats: null, // callbacks: [], options, compiler, watching: null, forceRebuild: false, }; function invalid(callback) { if (context.state) { context.options.reporter(context.options, { log, state: false, }); } // We are now in invalid state context.state = false; if (typeof callback === 'function') { callback(); } } // 关键代码 利用compile的hook 观察编译变化 并插入操做 context.compiler.hooks.invalid.tap('WebpackDevMiddleware', invalid); context.compiler.hooks.run.tap('WebpackDevMiddleware', invalid); context.compiler.hooks.done.tap('WebpackDevMiddleware', done); context.compiler.hooks.watchRun.tap( 'WebpackDevMiddleware', (comp, callback) => { invalid(callback); } );
核心的代码就是使用webpack
提供的内置hook
watchRun
来插入自定义的操做(检查编译状况,生成临时结果到内存)
呼~ tapable的内容大概写完了,本文介绍了同步的几种钩子,和异步的2种表明性的钩子,至于异步并行熔断等等,就是前面介绍的钩子的合成,比较简单。回顾一下主要的内容:
tapAsync/callAsync
和tapPromise/promise
两种使用方式,而且均可以观察事件总体执行结果;理解清楚tapable以后,再开始学习webpack的源码,会相对顺畅一些。
-----惯例偷懒分割线-----
若是以为写得很差/有错误/表述不明确,都欢迎指出
若是有帮助,欢迎点赞和收藏,转载请征得赞成后著明出处。若是有问题也欢迎私信交流,主页有邮箱地址
若是以为做者很辛苦,也欢迎打赏~