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
  • AsyncParallelBailHook

同步钩子使用

安装: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
    }
    }

mcgee0731
60 声望4 粉丝

不会做饭的程序猿不是一个好厨子