webpack 底层核心是一个独立的工具库tapable
webpack编译过程,可以看作 事件驱动型事件工作机制
核心的两个: 负责编译的complier,负责创建bundles的compilation都是tapable的实例对象
- 配置初始化
- 内容编译
- 输出编译后内容
tapable工作流程
- 实例化hook注册事件监听
- 通过hook触发事件监听
- 执行懒编译生成的可执行代码
hook本身是实例对象,在tapable库中存在几种不同类,每个类实例都是一种hook实例(钩子),不同钩子拥有不同执行特点,
从执行机制将hook分为同步和异步,异步分为并行串行两种模式
Hook不同机制执行特点
- Hook:普通钩子,监听器之间互相独立不干扰
- BailHook:熔断钩子,某个监听返回非undefined时后续不执行
- WaterfallHook:瀑布钩子,上一个监听的返回值可传递至下一个
- LoopHook:循环钩子,如果当前未返回false则一直执行
总结:
- tapable是个库,内部提供不同类,可以实例化出不同hook
- hook分为同步异步两类,无论哪类都包含上面四个执行特点
tapable库同步钩子
- SyncHook
- SyncBailHook
- SyncWaterfallHook
- SyncLoopHook
tapable库异步串行钩子
- AsyncSeriesHook
- AsyncSeriesBailHook
- AsyncSeriesWaterfallHook
tapable库异步并行钩子
- AsyncParallelHook
-
同步钩子使用
安装:yarn add tapable —dev
使用:导入->实例化->添加监听->触发监听测试SyncHook
// 导入 const { SyncHook } = require(‘tapable’) // 实例化 let hook = new SyncHook([‘name’,’age’]) // 添加事件监听 hook.tap(‘fn1’,function(name,age){ // 钩子监听触发时会执行此函数体 console.log(“SyncHook:fn1—>”,name,age); }) // 触发监听函数 hook.call(‘mcgee’,18)
流式监听,可以定义多个监听事件依次处理内容
const { SyncHook } = require(‘tapable’) let hook = new SyncHook([‘name’,’age’]) // 添加事件监听 hook.tap(‘fn1’,function(name,age){ console.log(“SyncHook:fn1—>”,name,age); // SyncHook:fn1—> mcgee 18 }) hook.tap(‘fn2’,function(name,age){ console.log(“SyncHook:fn2—>”,name+”爸爸”,age+30); // SyncHook:fn2—> mcgee爸爸 48 }) hook.call(‘mcgee’,18)测试SyncBailHook
注意fn1return了非undefined的值,因此fn2不会执行,熔断的含义
const { SyncBailHook } = require(‘tapable’) let hook = new SyncBailHook([‘name’,’age’]) hook.tap(‘fn1’,function(name,age){ console.log(“SyncBailHook:fn1—>”,name,age); return 1; }) hook.tap(‘fn2’,function(name,age){ console.log(“SyncBailHook:fn2—>”,name+”爸爸”,age+30); }) hook.call(‘mcgee’,18)测试SyncWaterfallHook
返回值会被下一个监听获取
const { SyncWaterfallHook } = require(‘tapable’) let hook = new SyncWaterfallHook([‘name’,’age’]) hook.tap(‘fn1’,function(name,age){ console.log(“SyncWaterfallHook:fn1—>”,name,age); // SyncWaterfallHook:fn1—> mcgee 18 return “爸爸” }) hook.tap(‘fn2’,function(name,age){ console.log(“SyncWaterfallHook:fn2—>”,name,age+30); // SyncWaterfallHook:fn2—> 爸爸 48 }) hook.call(‘mcgee’,18)测试SyncLoopHook
如果监听未返回undefined,则一直执行,重新从taps数组里的第一个tap开始执行可以断点看一下
const { SyncLoopHook } = require(‘tapable’) let hook = new SyncLoopHook([‘name’,’age’]) hook.tap(‘fn1’,function(name,age){ console.log(“SyncLoopHook:fn1—>”,name,age); if(++count1 === 1) { count1 = 0 return undefined } return true }) hook.tap(‘fn2’,function(name,age){ console.log(“SyncLoopHook:fn2—>”,name,age+30); }) hook.call(‘mcgee’,18)测试AsyncParallelHook
对于异步钩子并行的使用,在添加事件监听时会存在三种方式
tap
- tapAsync
- tapPromise
const { AsyncParallelHook } = require(‘tapable’) let hook = new AsyncParallelHook([‘name’])
添加事件监听 tap
hook.tap(‘fn1’,function(name){ // 钩子监听触发时会执行此函数体 console.log(“AsyncParallelHook:fn1—>”,name); }) hook.tap(‘fn2’,function(name){ console.log(“AsyncParallelHook:fn2—>”,name); })
触发监听函数
hook.callAsync(‘mcgee’,function(){ console.log(“run“); _// 执行的回调,依次输出fn1,fn2,run~_ console.timeEnd(‘time’) })
添加事件监听 tapAsync
console.time(‘time’) hook.tapAsync(‘fn1’,function(name,callback){ setTimeout(()=>{ console.log(“AsyncParallelHook:fn1—>”,name); callback() },1000) }) hook.tapAsync(‘fn2’,function(name,callback){ setTimeout(()=>{ console.log(“AsyncParallelHook:fn2—>”,name); callback() },2000) })
触发监听函数
hook.callAsync(‘mcgee’,function(){ console.log(“run~~”); console.timeEnd(‘time’) })
添加事件监听 tapPromise
console.time(‘time’) hook.tapPromise(‘fn1’,function(name){ return new Promise(function(resolve,reject){ setTimeout(()=>{ console.log(“AsyncParallelHook:fn1—>”,name); resolve() },1000) }) }) hook.tapPromise(‘fn2’,function(name){ return new Promise(function(resolve,reject){ setTimeout(()=>{ console.log(“AsyncParallelHook:fn2—>”,name); resolve() },2000) }) })
触发监听函数
hook.promise(‘foo’).then(()=>{ console.log(“run~~”); console.timeEnd(‘time’) })
测试AsyncParallelBailHook
熔断操作,通过callback函数错误优先,fn2不会执行
const { AsyncParallelBailHook } = require(‘tapable’) let hook = new AsyncParallelBailHook([‘name’]) console.time(‘time’) hook.tapAsync(‘fn1’,function(name,callback){ setTimeout(()=>{ console.log(“AsyncParallelBailHook:fn1—>”,name); callback(‘err’) // callback回调中错误优先 },1000) }) hook.tapAsync(‘fn2’,function(name,callback){ setTimeout(()=>{ console.log(“AsyncParallelBailHook:fn2—>”,name); callback() },2000) })
触发监听函数
hook.callAsync(‘mcgee’,function(){ console.log(“run~~”); console.timeEnd(‘time’) })
测试AsyncSeriesHook
串行,下面代码会执行3s
const { AsyncSeriesHook } = require(‘tapable’) let hook = new AsyncSeriesHook([‘name’])
console.time(‘time’) hook.tapPromise(‘fn1’,function(name){ return new Promise(function(resolve,reject){ setTimeout(()=>{ console.log(“AsyncSeriesHook:fn1—>”,name); resolve() },1000) }) }) hook.tapPromise(‘fn2’,function(name){ return new Promise(function(resolve,reject){ setTimeout(()=>{ console.log(“AsyncSeriesHook:fn2—>”,name); resolve() },2000) }) })
执行监听
hook.promise(‘foo’).then(()=>{ console.log(“run~~”); console.timeEnd(‘time’) })
基于SyncHook的源码分析
从源码可以看出,node_modules/tapable/lib/Hook.js文件为所有钩子的基类文件,
其他钩子都是继承自Hook.js并对Hook.js内的方法进行改写的操作,
tap会将注册的内容存成对象,{type:’async’,fn:f,name:’fn1’}再塞入Hook类的属性taps数组中
hook.tap(‘fn1’,function(name){ console.log(“AsyncParallelHook:fn1—>”,name); })
基于SyncHook的源码分析2
- 导入
- 实例化 SyncHook 的基类hook,两个重要属性 _x 和 taps
- _x = [ f1, f2… ] tap注册的回调内容
- taps = [ {}, {} ] tap数组
- 调用 call 方法,使用了 HookCodeFactory 类 setup create
- Hook SyncHook HookCodeFactory// # hook.js 基类 class Hook { constructor(args = []) { this.args = args this.taps = [] // 将来用于存放组装好的 {} this.x = undefined // 将来在代码工厂函数中会给 x = [f1, f2, f3….] } tap(options, fn) { if (typeof options === ‘string’) { options = { name: options } } options = Object.assign({ fn }, options) // { fn:… name:fn1 } // 调用以下方法将组装好的 options 添加至 [] this.insert(options) } _insert(options) { this.taps[this.taps.length] = options } call(…args) { // 01 创建将来要具体执行的函数代码结构 let callFn = this._createCall() // 02 调用上述的函数(args传入进去) return callFn.apply(this, args) } _createCall() { return this.compile({ taps: this.taps, args: this.args }) } } module.exports = Hook// # SyncHook.js let Hook = require(‘./Hook.js’) //———————————————————————————— // 代码组装工厂 HookCodeFactory 基于不同hook子类(SyncHook,SyncBailHook),设置不同函数体的类 class HookCodeFactory { args() { return this.options.args.join(‘,’) // [“name”, “age”]===> name, age } head() { return
var _x = this._x;
} content() { let code =` **for** (**var** i = 0; i < this.options.taps.length; i++) { code +=
var _fn${i} = _x[${i}];_fn${i}(${this.args()});` } return code } setup(instance, options) { // 先准备后续需要使用到的数据 this.options = options // 这里的操作在源码中是通过 init 方法实现,而我们当前是直接挂在了 this 身上 instance._x = options.taps.map(o => o.fn) // this.x = [f1, f2, ….] } create() { // 核心就是创建一段可执行的代码体然后返回 let fn // fn = new Function(“name, age”, “var _x = this._x, var _fn0 = _x[0]; _fn0(name, age);”) fn = new Function( this.args(), this.head() + this.content() ) return fn } } //———————————————————————————— let factory = new HookCodeFactory() // 子类 SyncHook class SyncHook extends Hook { constructor(args) { super(args) } compile(options) { // {taps: [{}, {}], args: [name, age]} factory.setup(this, options) return factory.create(options) } } module.exports = SyncHook使用上面的hook类// # useHook.js const SyncHook = require(‘./SyncHook.js’) let hook = new SyncHook([‘name’, ‘age’]) hook.tap(‘fn1’, function (name, age) { console.log(‘fn1—>’, name, age) }) hook.tap(‘fn2’, function (name, age) { console.log(‘fn2—>’, name, age) }) hook.call(‘zoe66’, 18) /* 01 实例化 hook , 定义 _x = [f1, f2, …] taps = [{}, {}] 02 实例调用 tap taps = [{}, {}] 03 调用 call 方法, HookCodeFactory setup create 04 Hook SyncHook HookCodeFactory /#### 基于AsyncParallelHook的源码分析 // # useHook.js const AsyncParallelHook = require(‘./AsyncParallelHook.js’) let hook = new AsyncParallelHook([‘name’, ‘age’]) hook.tapAsync(‘fn1’, function (name, age, callback) { console.log(‘fn1—>’, name, age) callback() }) hook.tapAsync(‘fn2’, function (name, age, callback) { console.log(‘fn2—>’, name, age) callback() }) hook.callAsync(‘zoe66’, 18)代码工程要修改的内容class HookCodeFactory { head() { return**"use strict"**;**var** _context;**var** _x = this._x;
} content() { let code =**var** _counter = ${this.options.taps.length};**var** _done = (**function** () { _callback(); });
for (var i = 0; i < this.options.taps.length; i++) { code +=**var** _fn${i} = _x[${i}];_fn${i}(name, age, (**function** () { **if** (--_counter === 0) _done(); }));
} return code } args({ after, before } = {}) { let allArgs = this.options.args if (before) allArgs = [before].concat(allArgs) if (after) allArgs = allArgs.concat(after) return allArgs.join(‘,’) // [“name”, “age”]===> name, age } }