webpack插件核心库 - Tapable

255 阅读7分钟

webpack的插件本质上是基于事件流的机制,它的工作流程就是将各个插件串连起来,而实现这一切的核心就是Tapable。Tapable有点类似于nodejs的EventEmitter库,核心原理也是依赖发布订阅模式。

此篇文章来源于自己多年前其他平台的记录文章

Tapable

Tabable提供了很多钩子类,可大致分为同步(sync*)和异步(async*)两种

    const {
        SyncHook,
        SyncBailHook,
        SyncWaterfallHook,
        SyncLoopHook,
        AsyncParallelHook,
        AsyncParallelBailHook,
        AsyncSeriesHook,
        AsyncSeriesBailHook,
        AsyncSeriesWaterfallHook
     } = require("tapable");

Tapable

安装

    yarn add tapable

见个面

所有的钩子构造函数都接受一个可选的参数(这个参数最好是数组,不是数组tapable内部也把他变成数组),这是一个参数的字符串名字列表。下面以简单的同步钩子为例,带大家先体验一下tapable的基本使用

    let { SyncHook } = require('Tapable')
    let hook = new SyncHook(['name']) //通过SyncHook实例化一个同步钩子,参数可选,最好传数组
    hook.tap('hello',function(name){  //注册函数
      console.log('hello ', name)
    })
    hook.tap('welcome',function(name){  //注册函数
      console.log('welcome', name)
    })
    hook.call('word')  //触发函数
    //hello word
    //welcome word

在案例中,我们通过SyncHook构建了一个同步钩子,然后调用tap注册了两个函数,再调用call依次触发注册的回调函数。

tap用于绑定同步钩子的API,绑定异步钩子需要使用tapAsync(绑定异步钩子的API)和tapPromise(绑定promise钩子的API)。同样的,call用于触发同步钩子,callAsync触发异步钩子,promise用于触发promise钩子

使用与实现

下面将介绍以下几个同步和异步钩子的使用,再简单的实现一下

特点概览

  • 同步钩子(sync*)
    • SyncHook: 同步串行,不关心订阅函数执行后的返回值是什么,在触发事件之后,会按照事件注册的先后顺序执行所有的事件处理函数。
    • SyncBailHook: 同步串行,执行过程中注册的回调返回非 undefined 时就停止不在执行(Bai保险)
    • SyncWaterfallHook: 同步串行瀑布流,上一个注册的回调返回值会作为下一个注册的回调的参数
    • SyncLoopHook: 同步串行,在执行过程中回调返回非 undefined 时继续再次执行当前的回调
  • 异步钩子(async*)
    • AsyncParallelHook: 异步并行,当注册的所有异步回调都并行执行完毕之后再执行 callAsync 或者 promise 中的函数
    • AsyncSeriesHook: 异步串行,顺序的执行异步函数
    • AsyncParallelBailHook: 异步并行,执行过程中注册的回调返回非 undefined 时就会直接执行 callAsync 或者 promise 中的函数(由于并行执行的原因,注册的其他回调依然会执行)
    • AsyncSeriesBailHook: 异步串行,执行过程中注册的回调返回非 undefined 时就会直接执行 callAsync 或者 promise 中的函数,并且注册的后续回调都不会执行
    • AsyncSeriesWaterfallHook: 异步串行瀑布流,与 SyncWaterfallHook 类似,上一个注册的异步回调执行之后的返回值会传递给下一个注册的回调

下面将分别引入Tapable库的这几个同步和异步钩子,查看执行结果,并着手实现一下相同的效果

同步钩子

SyncHook

  • 特点:同步串行,不关心订阅函数执行后的返回值是什么,在触发事件之后,会按照事件注册的先后顺序执行所有的事件处理函数。其原理是将监听(订阅)的函数存放到一个数组中, 发布时遍历数组中的监听函数并且将发布时的 arguments传递给监听函数
  • 使用:(请参考前面[见个面])
  • 实现:
    class SyncHook {
      constructor(args){
        //记录添加的hook(回调函数)
        this.hooks = []
      }
      tap(name,hook){
        //注册回调函数
        this.hooks.push(hook)
      }
      call(...args){
        //依次执行注册的回调函数
        this.hooks.forEach(hook=>hook(...args))
      }
    }

    // 使用
    let hook = new SyncHook(['name']) //通过SyncHook实例化一个同步钩子,参数可选,最好传数组
    hook.tap('hello',function(name){  //注册函数
      console.log('hello ', name)
    })
    hook.tap('welcome',function(name){  //注册函数
      console.log('welcome', name)
    })
    hook.call('word')
    //hello word
    //welcome word

SyncBailHook

  • 特点:同步串行,执行过程中注册的回调返回非 undefined 时就停止不在执行(Bai保险)
  • 使用:
    let { SyncBailHook } = require('Tapable')

    let hook = new SyncBailHook(['name'])

    hook.tap('hello',function(name){ 
      console.log('hello ', name)
      return '好久不见!'  // 返回值不为undefined时停止执行后面函数
    })
    hook.tap('welcome',function(name){
      console.log('welcome', name)
    })

    hook.call('word')
    //hello word
  • 实现
    class SyncBailHook{
      constructor(args){
        this.hooks = []
      }
      tap(name,hook){
        this.hooks.push(hook)
      }
      call(...args){
        if(this.hooks.length === 0){
          return
        }
        let ret; //当前函数返回值
        let index = 0; //从第一个还是开始执行
        do {
          ret = this.hooks[index++](...args)
        } while (ret === undefined && index < this.hooks.length);
      }
    }


    let hook = new SyncBailHook(['name'])

    hook.tap('hello',function(name){ 
      console.log('hello ', name)
      return ''好久不见!'  // 返回值不为undefined时停止执行后面函数
    })
    hook.tap('welcome',function(name){
      console.log('welcome', name)
    })

    hook.call('word')
    //hello word

SyncWaterfallHook

  • 特点:同步串行瀑布流,上一个注册的回调返回值会作为下一个注册的回调的参数
  • 使用:
    let { SyncWaterfallHook } = require('Tapable')

    let hook = new SyncWaterfallHook(['name'])

    hook.tap('hello',function(name){ 
      console.log('hello ', name)
      return '好久不见!'  // 返回值将作为后面的参数
    })
    hook.tap('welcome',function(name){
      console.log('welcome', name)  // 没用返回值,将返回参数
    })

    hook.tap('again',function(name){
      console.log('again', name)
    })

    hook.call('word')
    //hello  word
    //welcome 好久不见!
    //again 好久不见!
  • 实现
    class SyncWaterfallHook{
      constructor(args){
        this.hooks = []
      }
      tap(name,hook){
        this.hooks.push(hook)
      }
      call(...args){
        if(this.hooks.length === 0){
          return
        }
        let [first,...other] = this.hooks
        let ret = first(...args)
        other.reduce((a,b)=>{
          let rest = b(a)
          if(rest !== undefined){
            return rest
          }
          return a
        }, ret)
      }
    }


    let hook = new SyncWaterfallHook(['name'])

    hook.tap('hello',function(name){ 
      console.log('hello ', name)
      return '好久不见!'  // 返回值将作为后面的参数
    })
    hook.tap('welcome',function(name){
      console.log('welcome', name)  // 没用返回值,将返回参数
    })

    hook.tap('again',function(name){
      console.log('again', name)
    })

    hook.call('word')
    //hello  word
    //welcome 好久不见!
    //again 好久不见!

SyncLoopHook

  • 特点:同步串行,在执行过程中回调返回非 undefined 时继续再次执行当前的回调
  • 使用:
    let { SyncLoopHook } = require('Tapable')

    let hook = new SyncLoopHook(['name'])
    let index = 0

    hook.tap('hello',function(name){ 
      console.log('hello ', name)
      return ++index === 5 ? undefined : 'again'
    })
    hook.tap('welcome',function(name){
      console.log('welcome', name)
    })

    hook.call('word')
    //hello  word
    //hello  word
    //hello  word
    //hello  word
    //hello  word
    //welcome word
  • 实现:
    class SyncLoopHook{
      constructor(args){
        this.hooks = []
      }
      tap(name,hook){
        this.hooks.push(hook)
      }
      call(...args){
        this.hooks.forEach(hook=>{
          let ret;
          do {
            ret = hook(...args)
          } while (ret !== undefined);
        })
      }
    }


    let hook = new SyncLoopHook(['name'])
    let index = 0

    hook.tap('hello',function(name){ 
      console.log('hello ', name)
      return ++index === 5 ? undefined : 'again'
    })
    hook.tap('welcome',function(name){
      console.log('welcome', name)
    })

    hook.call('word')
    //hello  word
    //hello  word
    //hello  word
    //hello  word
    //hello  word
    //welcome word

异步钩子

AsyncParallelHook

  • 特点:异步并行,当注册的所有异步回调都并行执行完毕之后再执行 callAsync 或者 promise 中的函数
  • 使用:
    let { AsyncParallelHook } = require('Tapable')
    // -------------------------> callback
    let hook = new AsyncParallelHook(['name'])

    hook.tapAsync('hello',function(name,cb){   //tapAsync注册异步钩子
      setTimeout(() => {
        console.log('hello ', name)
        cb()
      }, 1000);

    })
    hook.tapAsync('welcome',function(name,cb){ 
      setTimeout(() => {
        console.log('welcome ', name)
        cb()
      }, 1000);
    })

    hook.callAsync('word',function(){  //触发异步钩子,当传了回调函数,并且前面调用的回调函数(就是前面的cb())次数等于注册的钩子个数时,会执行该回调函数,否则不执行
      console.log('end')
    })
    //hello  word
    //welcome  word
    //end

    // -----------------------> promise
    let hookPromise = new AsyncParallelHook(['name'])
    hookPromise.tapPromise('hello',function (name){ //tapPromise 注册promise钩子
      return new Promise((resolve,reject)=>{
        setTimeout(() => {
          console.log('hello ', name)
          resolve()
        }, 1000);
      })
    })
    hookPromise.tapPromise('welcome',function (name){
      return new Promise((resolve,reject)=>{
        setTimeout(() => {
          console.log('welcome ', name)
          resolve()
        }, 1000);
      })
    })
    hookPromise.promise('name').then(function(){  //触发promise钩子
      console.log('end')
    })
    //hello  word
    //welcome  word
    //end
  • 实现:
    class AsyncParallelHook{
      constructor(args){
        this.hooks = []
      }
      // -------------------> callback
      tapAsync(name,hook){
        this.hooks.push(hook)
      }
      callAsync(...args){
        let finalCallback = args.pop()
        let index = 0
        let done = () => {
          if(++index === this.hooks.length){
            finalCallback()
          }
        }
        this.hooks.forEach(hook => {
          hook(...args,done)
        });
      }
      // ------------------->  promise
      tapPromise(name,hook){
        this.hooks.push(hook)
      }
      promise(...args){
        let hooks = this.hooks.map(hook=>hook(...args))
        return Promise.all(hooks)
      }
    }

    // ---------------------> callback
    let hook = new AsyncParallelHook(['name'])

    hook.tapAsync('hello',function(name,cb){   //tapAsync注册异步钩子
      setTimeout(() => {
        console.log('hello ', name)
        cb()
      }, 1000);

    })
    hook.tapAsync('welcome',function(name,cb){ 
      setTimeout(() => {
        console.log('welcome ', name)
        cb()
      }, 1000);
    })

    hook.callAsync('word',function(){  //触发异步钩子,当传了回调函数,并且前面调用的回调函数(就是前面的cb())次数等于注册的钩子个数时,会执行该回调函数,否则不执行
      console.log('end')
    })
    //hello  word
    //welcome  word
    //end

    // --------------------> promise
    let hookPromise = new AsyncParallelHook(['name'])

    hookPromise.tapPromise('hello',function (name){ //tapPromise 注册promise钩子
      return new Promise((resolve,reject)=>{
        setTimeout(() => {
          console.log('hello ', name)
          resolve()
        }, 1000);
      })
    })

    hookPromise.tapPromise('welcome',function (name){
      return new Promise((resolve,reject)=>{
        setTimeout(() => {
          console.log('welcome ', name)
          resolve()
        }, 1000);
      })
    })

    hookPromise.promise('word').then(function(){  //触发promise钩子
      console.log('end')
    })
    //hello  word
    //welcome  word
    //end

AsyncParallelBailHook

  • 特点:异步并行,执行过程中注册的回调返回非 undefined 时就会直接执行 callAsync 或者 promise 中的函数(由于并行执行的原因,注册的其他回调依然会执行)
  • 使用:
    // 后续
  • 实现:
    // 后续

AsyncSeriesBailHook

  • 特点:异步串行,执行过程中注册的回调返回非 undefined 时就会直接执行 callAsync 或者 promise 中的函数,并且注册的后续回调都不会执行
  • 使用:
    // 后续
  • 实现:
    // 后续

AsyncSeriesHook

  • 特点:异步串行,顺序的执行异步函数
  • 使用:
    // 后续
  • 实现:
    // 后续

AsyncSeriesWaterfallHook

  • 特点:异步串行瀑布流,与 SyncWaterfallHook 类似,上一个注册的异步回调执行之后的返回值会传递给下一个注册的回调
  • 使用:
    // 后续
  • 实现:
    // 后续