上一章咱们有提到webpack支持特定的配置,来监控其编译进度,那么这个机制是怎么实现的呢?webpack
webpack整个构建周期,会涉及不少个阶段,每一个阶段都对应着一些节点,这些节点就是咱们常说的钩子,每个钩子上挂载着一些插件,能够说整个webpack生态系统是由一系列的插件组成的。当主构建流程进行编译打包的时候,会陆续触发一些钩子的call方法(至关于emitter),相应的插件(至关于listener)就会获得执行,webpack将这个机制封装为一个库,就是Tapable,webpack的核心对象Compiler和Complation均是Tapable的实例。web
Tapable提供了不少类型的hook,主要分为两大类:同步和异步,异步又分为串行(前一个异步执行完才会执行下一个)和并行(等待全部并发的异步事件执行完以后才会执行最后的回调)。在webpack里边(Compiler和Compilation)主要使用了SyncHook、SyncBailHook、AsyncSeriesHook三种钩子,所以这里咱们只着重介绍这三种。api
咱们参考的版本是webpack中使用的版本v1.1.3,这里主要是Hook.js、SyncHook.js、HookCodeFactory.js三个文件,能够供你们参考。bash
源码里边,每个文件对应一个类型的钩子。每一种钩子都是基于Hook和HookCodeFactory两个类。markdown
class SyncHook{ constructor(arg) { if (Array.isArray(arg)) { this.args = arg } else { this.args = [arg] } this.funs = [] this.taps = [] this.interceptors = [] } intercept(interceptor) { this.interceptors.push(Object.assign({}, interceptor)) // 若是有多个register拦截器 老是以最后一个拦截器为准 if (interceptor.register) { // 全部Tap对象依赖于register函数的返回值 for(let i in this.taps) { this.taps[i] = interceptor.register(this.taps[i]) } } } tap(option, fn) { if (typeof option === 'string') { option = { name: option } } if (typeof option !== 'object' || option === null) { throw new Error("Invalid arguments to tap(options: Object, fn: function)"); } option = Object.assign({ type: "sync", fn: fn }, option); if (typeof option.name !== 'string' || option.name === '') { throw new Error("Missing name for tap"); } option = this._putInterceptor(option) this.taps.push(option) // 根据stage属性进行排序 决定taps的触发顺序 if (this.taps.length > 1) { this.taps = this._sort(this.taps) } this.funs = this.taps.map(item => item.fn) } call(...args) { // 以初始化钩子时传入的参数长度为准,多余的参数无效 const _args = args.slice(0, this.args.length) for(let i in this.funs) { const curTap = this.taps[i] const curFun = this.funs[i] // 根据Tap对象的属性 决定是否要传入上下文 if (curTap.context) { curFun.call(this, curTap, ..._args) } else { curFun.call(this, ..._args) } } } _putInterceptor(option) { // 若tap的时候已经存在拦截器 则替换Tap对象 for(let interceptor of this.interceptors) { if (interceptor.register) { const newOption = interceptor.register(option) if (!newOption) { option = newOption } } } return option } _sort(taps) { return taps.sort((cur, next) => cur.stage - next.stage) } } // 使用 const hook = new SyncHook(["arg1",'arg2']); hook.intercept({ register: (tap) => { tap.name = 'changed' return tap }, }) hook.tap({ name: 'A', context: true, stage: 22, }, (context, arg1) => { console.log('A:', arg1) }) hook.call('arg1', 'arg2')复制代码
// 只须要在SyncHook的基础上改动一下call方法 class SyncBailHook() { // some code... call(...args) { // 以初始化钩子时传入的参数长度为准,多余的参数无效 const _args = args.slice(0, this.args.length) let ret for(let i in this.funs) { if (ret !== undefined) { break } else { const curTap = this.taps[i] const curFun = this.funs[i] // 根据Tap对象的属性 决定是否要传入上下文 if (curTap.context) { ret = curFun.call(this, curTap, ..._args) } else { ret = curFun.call(this, ..._args) } } } } // some code... } const hook = new SyncBailHook(['arg']) hook.tap('A', (param) => { console.log('A:',param) // 只有当返回值为undefined时 才会执行后边的钩子 return 1 }) hook.tap('B', (param) => console.log('B:', param)) hook.call('hi')复制代码
// 只需在SyncHook的基础上新增两个方法 class AsyncSeriesHook{ // some code ... // 同tap方法 tapAsync(option, fn) { // some code ... } callAsync(...args) { const finalCb = args.pop() let index = 0 let next = () => { if (this.funs.length == index) { return finalCb() } let curFun = this.funs[index++] curFun(...args, next) } next() } // some code ... } const hook = new AsyncSeriesHook(['arg']) hook.tapAsync('A', (param, cb) => { setTimeout(() => { console.log('A', param) cb() // 必须调用cb 才会执行后续的钩子 }, 1000) }) hook.tapAsync('B', (param, cb) => { setTimeout(() => { console.log('B') cb() }, 0) }) hook.callAsync('hi', () => { console.log('end') })复制代码