Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

webpack tapable介绍及简单实现 #41

Open
zgfang1993 opened this issue Mar 8, 2020 · 0 comments
Open

webpack tapable介绍及简单实现 #41

zgfang1993 opened this issue Mar 8, 2020 · 0 comments

Comments

@zgfang1993
Copy link
Owner

zgfang1993 commented Mar 8, 2020

tapable

webpack本质是事件流机制,工作流程就是将各个插件串联起来,实现这个的核心就是tabable。类似于nodejs的核心也是发布订阅模式。

webpack中最核心的负责编译的Compiler和负责创建bundles的Compilation都是Tapable的实例。

hooks概览

常用的钩子函数包含以下几种,主要分为同步钩子和异步钩子,异步钩子又分为串行和并行。

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

tapable

同步:

  • SyncHook 不关心返回值
  • SyncBailHook 返回非undefined的会停止
  • SyncWaterfallHook 会把上一个返回的值作为参数传给下一个
  • SyncLoopHook 遇到不返回undefined的函数反复执行

异步:

​ 并发

  • AsyncParallelHook 不关心返回值

  • AsyncParallelBailHook 带保险钩子

    串行

  • AsyncSeriesHook 不关心返回值

  • AsyncSeriesBailHook

  • AsyncSeriesWaterfallHook 异步串行瀑布钩子函数

注册方法:

  • tap: 同步注册
  • tapAsync:异步注册
  • tapPromise:注册是promise

同步Hooks介绍

下面具体说说每个hook的功能,以及如何简单实现。

SyncHook 同步钩子

写一个简单的SyncHook同步钩子

使用示例:

let {SyncHook} = require('tapable');

class Dinner {
  constructor(){
    this.hook = new SyncHook(['name'])
  }
  // 注册监听
  tap(){
    this.hook.tap('order1', function(name){
      console.log('来订单了 order1,下单人:', name)
    })
    this.hook.tap('order2', function(name){
      console.log('来订单了 order2,下单人:', name)
    })
  }
  // 触发执行
  start(){
    this.hook.call('sherry')
  }
}

const dinner = new Dinner();

dinner.tap(); // 会把注册函数放到一个数组里
dinner.start(); // 触发后把注册的函数依次执行

打印信息:

来订单了 order1,下单人: sherry
来订单了 order2,下单人: sherry

简单实现:

class SyncHook {
  constructor(){
    this.tasks = [];
  }
  tap(name, task){
    this.tasks.push(task);
  }
  call(...args){
    // 依次执行注册的函数
    this.tasks.forEach(task => {
      task(...args)
    })
  }
}

const hook = new SyncHook();

hook.tap('order1', function(name){
  console.log('来订单了 order1,下单人:', name)
})
hook.tap('order2', function(name){
  console.log('来订单了 order1,下单人:', name)
})

hook.call('sherry');

SyncBailHook

写同步时可以加保险,在注册的函数里,可以决定是否向下执行。 返回非undefined的值,不会向下执行了。

使用示例:

let {SyncBailHook} = require('tapable');

class Dinner {
  constructor(){
    this.hook = new SyncBailHook(['name'])
  }

  // 注册监听
  tap(){
    this.hook.tap('order1', function(name){
      console.log('来订单了 order1,下单人:', name)
      
      return '订单出问题了' // 返回非undefined的值,不会向下执行了
      // return undefined // 会向下执行
    })
    this.hook.tap('order2', function(name){
      console.log('来订单了 order2,下单人:', name)
    })
  }

  // 触发执行
  start(){
    this.hook.call('sherry')
  }
}

const dinner = new Dinner();

dinner.tap(); // 会把注册函数放到一个数组里
dinner.start(); // 触发后把注册的函数依次执行

打印信息:

来订单了 order1,下单人: sherry

简单实现:

class SyncBailHook {
  constructor() {
    this.tasks = [];
  }
  tap(name, task) {
    this.tasks.push(task);
  }
  call(...args) {
    let ret;
    let index = 0;

    do {
      ret = this.tasks[index++](...args)
    } while (ret === undefined && index < this.tasks.length)
  }
}

const hook = new SyncBailHook();

hook.tap('order1', function (name) {
  console.log('来订单了 order1,下单人:', name)

  return '订单出问题了' // 返回非undefined的值,不会向下执行了
  // return undefined // 会向下执行
})
hook.tap('order2', function (name) {
  console.log('来订单了 order2,下单人:', name)
})

hook.call('sherry');

SyncWaterfallHook

可以传递信息给下一个注册的函数。

使用示例:

let {SyncWaterfallHook} = require('tapable');

class Dinner {
  constructor(){
    this.hook = new SyncWaterfallHook(['name'])
  }
  // 注册监听
  tap(){
    this.hook.tap('order1', function(name){
      console.log('订单1 开始配送,配送人:', name)
    })
    this.hook.tap('order2', function(data){
      console.log('订单2 开始配送,配送人:', data)

      return {
        msg: '这一单超时了,下一单要快点',
        name: data
      }
    })
    this.hook.tap('order3', function(data){
      console.log('订单3 开始配送,配送人:', data.name, `备注:${data.msg}`)
    })
  }
  start(){
    this.hook.call('sherry')
  }
}

const dinner = new Dinner();

dinner.tap();
dinner.start();

打印信息:

订单1 开始配送,配送人: sherry
订单2 开始配送,配送人: sherry
订单3 开始配送,配送人: sherry 备注:这一单超时了,下一单要快点

简单实现:

需要把上一个函数的return的结果,传给下一个函数作为参数。

class SyncWaterfallHook {
  constructor() {
    this.tasks = [];
  }
  tap(name, task) {
    this.tasks.push(task);
  }
  call(...args) {
    const [first, ...others] = this.tasks;
    const ret = first(...args);

    others.reduce((a, b) => {
      return a ? b(a) : b(...args) // 如果没有return,还是传默认参数
    }, ret)
  }
}

const hook = new SyncWaterfallHook();

hook.tap('order1', function (name) {
  console.log('订单1 开始配送,配送人:', name)
})
hook.tap('order2', function (data) {
  console.log('订单2 开始配送,配送人:', data)

  return {
    msg: '这一单超时了,下一单要快点',
    name: data
  }
})
hook.tap('order3', function (data) {
  console.log('订单3 开始配送,配送人:', data.name, `备注:${data.msg}`)
})

hook.call('sherry');

SyncLoopHook

webpack暂时没有用到这个方法。

若某个注册函数不返回undefined,这个函数及这个函数前面的函数会重复多次执行,直到返回undefined。

使用示例:

let {SyncLoopHook} = require('tapable');

class Dinner {
  constructor(){
    this.index = 0
    this.hook = new SyncLoopHook(['name'])
  }
  // 注册监听
  tap(){
    this.hook.tap('order1', function(name){
      console.log('订单1 开始配送,配送人:', name)
    })

    this.hook.tap('order1', (name) => {
      console.log('订单2 开始配送,配送人:', name)
      this.index ++;

      return this.index === 3 ? undefined : '外卖订单凉了,请重新配送'
    })

    this.hook.tap('order3', function(name){
      console.log('订单3 开始配送,配送人:', name)
    })
  }
  start(){
    this.hook.call('sherry')
  }
}

const dinner = new Dinner();

dinner.tap();
dinner.start();

打印信息:

前两个注册函数重复执行了3次,才继续

订单1 开始配送,配送人: sherry
订单2 开始配送,配送人: sherry
订单1 开始配送,配送人: sherry
订单2 开始配送,配送人: sherry
订单1 开始配送,配送人: sherry
订单2 开始配送,配送人: sherry
订单3 开始配送,配送人: sherry

简单实现:

暂时只实现了当前函数重复执行

class SyncLoopHook {
  constructor() {
    this.tasks = [];
  }
  tap(name, task) {
    this.tasks.push(task);
  }
  call(...args) {
    this.tasks.forEach(task => {
      let ret;
      
      do {
        ret = task(...args)
      } while (ret !== undefined)
    })
  }
}

const hook = new SyncLoopHook();

let index = 0;
hook.tap('order1', function (name) {
  console.log('订单1 开始配送,配送人:', name)

})
hook.tap('order2', function (name) {
  console.log('订单2 开始配送,配送人:', name)
  index++;

  return index === 3 ? undefined : '外卖订单凉了,请重新配送'
})
hook.tap('order3', function (name) {
  console.log('订单3 开始配送,配送人:', name)

})
hook.call('sherry');

打印信息:

订单1 开始配送,配送人: sherry
订单2 开始配送,配送人: sherry
订单2 开始配送,配送人: sherry
订单2 开始配送,配送人: sherry
订单3 开始配送,配送人: sherry

异步Hooks介绍

异步注册使用tapAsync/tapPromise方法,触发调用使用callAsync/promise方法,需要提供回调函数。

AsyncParallelHook 异步并行

所有函数同时执行,等待所有并发的异步事件都执行完了,再执行回调函数。

使用示例(tapAsync):

只有注册的函数都执行了cb(),才会执行回调函数。有任何一个函数没有执行cb(),回调函数都不会执行。

const {AsyncParallelHook} = require('tapable');

class Dinner {
  constructor(){
    this.index = 0
    this.hook = new AsyncParallelHook(['name'])
  }
  // 注册监听
  tap(){
    this.hook.tapAsync('order1', function(name,cb){
      setTimeout(()=>{
        console.log('订单1配送需要1分钟', name)
        cb()
      }, 1000)
    })
    this.hook.tapAsync('order2', (name, cb) => {
      setTimeout(()=>{
        console.log('订单2配送需要2分钟', name)
        cb()
      }, 2000)
    })
  }
  start(){
    // 只有注册的函数都执行了`cb()`,才会执行回调函数。有任何一个函数没有执行`cb()`,回调函数都不会执行。
    this.hook.callAsync('sherry', ()=>{
      console.log('一共配送了2分钟。只有注册函数都执行了cb(), 我才会被执行')
    })
  }
}

const dinner = new Dinner();

dinner.tap();
dinner.start();

打印信息:

订单1配送需要1分钟 sherry
订单2配送需要2分钟 sherry
一共配送了2分钟。只有注册函数都执行了cb(), 我才会被执行

简单实现:

class AsyncParallelHook {
  constructor(){
    this.tasks = [];
  }
  tapAsync(name, task){
    this.tasks.push(task);
  }
  callAsync(...args){
    const finalCallback = args.pop() // 回调函数,都执行完才执行回调函数
    let index=0

    const done = ()=>{ // 类似于promise.all
      index++
      if(index === this.tasks.length){
        finalCallback()
      }
    }

   this.tasks.forEach(task => {
     task(...args, done)
   })
  }
}

const hook = new AsyncParallelHook();
let index = 0;

hook.tapAsync('order1', function(name, cb){
  setTimeout(()=>{
    console.log('订单1配送需要1分钟', name)
    cb()
  }, 1000)
})
hook.tapAsync('order2', function(name, cb){
  setTimeout(()=>{
    console.log('订单1配送需要2分钟', name)
    cb()
  }, 2000)
})

hook.callAsync('sherry', ()=>{
  console.log('一共配送了2分钟。只有注册函数都执行了cb(), 我才会被执行')
});

使用示例(tapPromise):

let {AsyncParallelHook} = require('tapable');

class Dinner {
  constructor(){
    this.index = 0
    this.hook = new AsyncParallelHook(['name'])
  }
  // 注册监听
  tap(){
    this.hook.tapPromise('order1', function(name){
      return new Promise((resolve, reject)=>{
        setTimeout(()=>{
          console.log('订单1配送需要1分钟', name)
          resolve();
        }, 1000)
      })
    })
    this.hook.tapPromise('order2', (name) => {
      return new Promise((resolve, reject)=>{
        setTimeout(()=>{
          console.log('订单2配送需要2分钟', name)
          resolve();
        }, 2000)
      })
    })
  }
  start(){
    this.hook.promise('sherry').then(()=>{
      console.log('结束啦,注册函数都resolve,我才会执行')
    })
  }
}

const dinner = new Dinner();
dinner.tap();
dinner.start();

打印信息:

订单1配送需要1分钟 sherry
订单2配送需要2分钟 sherry
结束啦,注册函数都resolve,我才会执行

简单实现:

使用Promise.all(tasks)来实现。

class AsyncParallelHook {
  constructor(){
    this.tasks = [];
  }
  tapPromise(name, task){
    this.tasks.push(task);
  }
  promise(...args){
    let tasks = this.tasks.map(task => task(...args));

    return Promise.all(tasks)
  }
}

const hook = new AsyncParallelHook();
let index = 0;

hook.tapPromise('order1', function(name){
  return new Promise((resolve, reject)=>{
    setTimeout(()=>{
      console.log('订单1配送需要1分钟', name)
      resolve();
    }, 1000)
  })
})

hook.tapPromise('order2', function(name){
  return new Promise((resolve, reject)=>{
    setTimeout(()=>{
      console.log('订单2配送需要2分钟', name)
      resolve();
    }, 2000)
  })
})

hook.promise('sherry').then(()=>{
  console.log('结束啦,注册函数都resolve,我才会执行')
});

AsyncSeriesHook 异步串行

注册函数之间有依赖,下一个函数依赖上一个函数返回的结果。

使用示例(tapAsync):

let {AsyncSeriesHook} = require('tapable');

class Dinner {
  constructor(){
    this.hook = new AsyncSeriesHook(['name'])
  }
  // 注册监听
  tap(){
    this.hook.tapAsync('order1', function(name, cb){
      setTimeout(()=>{
        console.log('订单1配送需要1分钟', name)
        cb() // 执行cb,才会往下走
      },1000)
    })
    this.hook.tapAsync('order2', function(name, cb){
      setTimeout(()=>{
        console.log('订单2配送需要2分钟', name)
        cb()// 最后一个函数,执行cb,才会往下走,调用call的回调函数
      },2000)
    })
  }
  start(){
    this.hook.callAsync('sherry', () => {
      console.log('一共执行了3分钟,需要所有注册函数都调用cb(),我才会被执行')
    })
  }
}

const dinner = new Dinner();
dinner.tap();
dinner.start();

打印信息:

订单1配送需要1分钟 sherry
订单2配送需要2分钟 sherry
一共执行了3分钟,需要所有注册函数都调用cb(),我才会被执行

简单实现:

class AsyncSeriesHook {
  constructor(){
    this.tasks = [];
  }
  tapAsync(name, task){
    this.tasks.push(task);
  }
  callAsync(...args){
    const finalcallback = args.pop();
    let index = 0;
    // 这个写法有点像express
    // 异步迭代,可以使用中间函数来实现
    const next = ()=>{
      if(this.tasks.length === index){
        finalcallback()
      }else{
        let task = this.tasks[index++];
        task(...args, next)
      }
    } 

    next();
  }
}

const hook = new AsyncSeriesHook();
let index = 0;

hook.tapAsync('node', function(name, cb){
  setTimeout(()=>{
    console.log('订单1配送需要1分钟', name)
    cb() // 执行cb,才会往下走
  }, 1000)
})
hook.tapAsync('react', function(name, cb){
  setTimeout(()=>{
    console.log('订单2配送需要2分钟', name)
    cb() // 最后一个函数,执行cb,才会往下走,调用call的回调函数
  }, 2000)
})

hook.callAsync('sherry', () => {
  console.log('一共执行了3分钟,需要所有注册函数都调用cb(),我才会被执行')
});

使用示例(tapPromise):

let {AsyncSeriesHook} = require('tapable');

class Dinner {
  constructor(){
    this.hook = new AsyncSeriesHook(['name'])
  }
  // 注册监听
  tap(){
    this.hook.tapPromise('order1', function(name, cb){
      return new Promise((resolve, reject)=>{
        setTimeout(()=>{
          console.log('订单1配送需要1分钟', name)
          resolve(); // 执行resolve,才会往下走
        }, 1000)
      })
    })
    this.hook.tapPromise('order2', function(name, cb){
      return new Promise((resolve, reject)=>{
        setTimeout(()=>{
          console.log('订单2配送需要2分钟', name)
          resolve(); // 执行resolve,才会往下走
        }, 2000)
      })
    })
  }
  start(){
    this.hook.promise('sherry').then(()=>{
      console.log('结束啦,一共需要3分钟,注册函数都resolve,我才会执行')
    })
  }
}

const dinner = new Dinner();

dinner.tap();
dinner.start();

打印信息:

订单1配送需要1分钟 sherry
订单2配送需要2分钟 sherry
结束啦,一共需要3分钟,注册函数都resolve,我才会执行

简单实现:

class AsyncSeriesHook {
  constructor(){
    this.tasks = [];
  }
  tapPromise(name, task){
    this.tasks.push(task);
  }
  promise(...args){
    const [first, ...others] = this.tasks;
    // 类似于redux源码
    return others.reduce((p, n) => {
      return p.then(()=>n(...args))
    }, first(...args))
  }
}

const hook = new AsyncSeriesHook();
let index = 0;

hook.tapPromise('node', function(name){
  return new Promise((resolve, reject)=>{
    setTimeout(()=>{
      console.log('订单1配送需要1分钟', name)
      resolve(); // 执行resolve,才会往下走
    }, 1000)
  })
})
hook.tapPromise('react', function(name){
  return new Promise((resolve, reject)=>{
    setTimeout(()=>{
      console.log('订单2配送需要2分钟', name)
      resolve(); // 执行resolve,才会往下走
    }, 2000)
  })
})

hook.promise('sherry').then(() => {
  console.log('结束啦,一共需要3分钟,注册函数都resolve,我才会执行')
});

AsyncSeriesWaterfallHook

第一个异步先执行,把结果传给下一个异步执行。若error,会略过后面,直接直接回调。

cb('error'):发生错误,不往下执行,回调函数仍然会执行

cb()/cb(null)/cb(null, data):往下继续执行,可以穿参数给下一个函数

使用示例(tapAsync):

let {AsyncSeriesWaterfallHook} = require('tapable');

class Dinner {
  constructor(){
    this.hook = new AsyncSeriesWaterfallHook(['name'])
  }
  // 注册监听
  tap(){
    this.hook.tapAsync('order1', function(name, cb){
      setTimeout(()=>{
        console.log('订单1配送需要1分钟', name)
        cb(undefined, { // 往下执行,并给下一个函数传参
          msg: '配送超时,下次快点',
          name
        })
        // cb('error') // 出错了,不往下执行了
      },1000)
    })
    this.hook.tapAsync('order2', function(data, cb){
      setTimeout(()=>{
        console.log('订单2配送需要2分钟', data.name, `备注:${data.msg}`)
        cb()
      },2000)
    })
  }
  start(){
    this.hook.callAsync('sherry', () => {
      console.log('一共执行了3分钟,需要所有注册函数都调用cb()或者某个函数执行cb("error"),我才会被执行')
    })
  }
}

const dinner = new Dinner();

dinner.tap();
dinner.start();

打印信息:

订单1配送需要1分钟 sherry
订单2配送需要2分钟 sherry 备注:配送超时,下次快点
一共执行了3分钟,需要所有注册函数都调用cb()或者某个函数执行cb("error"),我才会被执行

若调用cb('error'),打印信息:

订单1配送需要1分钟 sherry
一共执行了3分钟,需要所有注册函数都调用cb()或者某个函数执行cb("error"),我才会被执行

简单实现:

class AsyncSeriesWaterfallHook {
  constructor(){
    this.tasks = [];
  }
  tapAsync(name, task){
    this.tasks.push(task);
  }
  callAsync(...args){
    let index=0
    const finalCallback = args.pop()
    // 异步迭代使用中间函数来实现
    const next = (err, data) => {
      let task = this.tasks[index];

      if(!task || err) return finalCallback(); // 没有task或报错,执行执行回调函数
      if(index === 0){
        task(...args, next) // 第一个函数,需要传args
      }else {
        task(data, next) // 剩余函数,需要传入上一个函数执行cb的第二个值
      }

      index ++
    }

    next();
  }
}

const hook = new AsyncSeriesWaterfallHook();

hook.tapAsync('order1', function(name, cb){
  setTimeout(()=>{
    console.log('订单1配送需要1分钟', name)
    cb(undefined, { // 往下执行,并给下一个函数传参
      msg: '配送超时,下次快点',
      name
    })
    // cb('error') // 出错了,不往下执行了
  }, 1000)
})
hook.tapAsync('order2', function(data, cb){
  setTimeout(()=>{
    console.log('订单2配送需要2分钟', data.name, `备注:${data.msg}`)
    cb()
  }, 2000)
})

hook.callAsync('sherry', ()=>{
  console.log('一共执行了3分钟,需要所有注册函数都调用cb()或者某个函数执行cb("error"),我才会被执行')
});
@zgfang1993 zgfang1993 changed the title webpack tapable介绍 webpack tapable介绍及简单实现 Mar 8, 2020
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant