25.重学webpack——梳理tapable的九大钩子函数的使用及原理

【重学webpack系列——webpack5.0】

1-15节主要讲webpack的使用,当然,建议结合《webpack学完这些就够了》一起学习。
从本节开始,专攻webpack原理,只有深入原理,才能学到webpack设计的精髓,从而将技术点运用到实际项目中。
可以点击上方专栏订阅哦。

以下是本节正文:


  • webpack实现插件的流程:
  • 创建 - webpack在其内部对象上创建各种钩子Hook
  • 注册 - 插件讲自己的方法注册到对应的钩子Hook上,交给webpack
  • 调用 - webpack编译过程中,会适时地触发响应钩子Hook,因此也就触发了插件的方法。
  • webpack本质是一种事件流的机制,它的工作流程就是将各个插件串联起来,实现这个的核心是Tapable,webpack中最核心的负责编译Compiler和负责创建bundleCompliation都是Tapable的实例。

1.hook的分类-按运行顺序分

根据hook的类型可以分为同步Sync和异步Async,异步又可以分为并行串行

  • Tapable
    • 同步Hook
      • SyncHook
      • SyncBailHook
      • SyncWaterfallHook
      • SyncLoopHook
    • 异步钩子
      • 异步并行
        • AsyncParallelHook
        • AsyncParallelBailHook
      • 异步串行
        • AsyncSeriesHook
        • AsyncSeriesBailHook
        • AsyncSeriesWaterHook

2.hook的分类-按结果分

根据返回的类型可以分为Basic、Bail、Waterfall、Loop

  • Tapable
    • Basic 按顺序执行每个事件函数,不关系函数的返回值
      • SyncHook
      • AsyncParallelHook
      • AsyncSeriesHook
    • Bail 执行每个事件函数,遇到结果不为undefined则结束执行,直接走call的回调
      • SyncBailHook
      • AsyncParallelBailHook
      • AsyncSeriesBailHook
    • Waterfall 将最近上一个函数的返回值作为下一个函数的参数,如果上一个函数没有返回值(返回undefined),那么就继续找上上个,如果找不到就用自己传入的参数
      • SyncWaterfallHook
      • AsyncSeriesWaterfallHook
    • Loop 不停的循环执行事件函数,直到所有函数结果 result === undefined,每次循环都是从头开始的
      • SyncLoopHook

3.九大Hook使用

1.SyncHook
const { SyncHook } = require('tapable');
const syncHook = new SyncHook(['name', 'age222']); // ['name', 'age']代表形参。其实这两的参数有用的是参数的长度,代表取call的参数的个数,至于元素是什么 没什么关系
// 注册事件函数
syncHook.tap('1', (name, age) => {
  console.log(1, name, age);
})
syncHook.tap('2', (name, age) => {
  setTimeout(() => {
    console.log(2, name, age);
  })
  return 1
})
syncHook.tap('3', (name, age) => {
  console.log(3, name, age);
})
// 触发事件函数
syncHook.call('yh', 18);
2.SyncBailHook
const { SyncBailHook } = require('tapable')

const syncBailHook = new SyncBailHook(['name', 'age']);

syncBailHook.tap('1', (name, age) => {
  console.log(1, name, age)
})
syncBailHook.tap('2', (name, age) => {
  console.log(2, name, age)
  return '2'
})
syncBailHook.tap('3', (name, age) => {
  console.log(3, name, age)
})

syncBailHook.call('yh', 18)
3.SyncWaterfallHook
const { SyncWaterfallHook } = require('./myHook/3.SyncWaterfailHook');

const syncWaterfallHook = new SyncWaterfallHook(['name', 'age']);

syncWaterfallHook.tap('1', (name, age) => {
  console.log(1, name, age)
  return '名字1'
})
syncWaterfallHook.tap('2', (name, age) => {
  console.log(2, name, age)
  return '名字2'
})
syncWaterfallHook.tap('3', (name, age) => {
  console.log(3, name, age)
  return '名字3'
})
syncWaterfallHook.tap('4', (name, age) => {
  console.log(4, name, age)
})

syncWaterfallHook.call('yh', 18)

// waterfallHook将最近上一个函数的返回值作为下一个函数的参数,如果上一个函数没有返回值(返回undefined),那么就继续找上上个,如果找不到就用自己传入的参数
4.SyncLoopHook
const { SyncLoopHook } = require('./myHook/4.SyncLoopHook');
// 不停的循环执行事件函数,直到所有函数结果 result === undefined
// 每次循环都是从头开始的
const syncLoopHook = new SyncLoopHook(['name', 'age']);

let counter1 = 0;
let counter2 = 0;
let counter3 = 0;


syncLoopHook.tap('1', (name, age) => {
  console.log(1, 'counter1', counter1, name, age);
  if (++counter1 === 1) {
    counter1 = 0;
    return;
  }
  return true;
})
syncLoopHook.tap('2', (name, age) => {
  console.log(2, 'counter2', counter2, name, age);
  if (++counter2 === 2) {
    counter2 = 0;
    return;
  }
  return true;
})
// syncLoopHook.tap('3', (name, age) => {
//   console.log(3, 'counter3', counter3, name, age);
//   if (++counter3 === 3) {
//     counter3 = 0;
//     return;
//   }
//   return true;
// })
syncLoopHook.call('yh', 18);
5.AsyncParallelHook

该钩子有三种注册监听事件的方式:

  1. tap 注册同步钩子,按顺序执行回调,不需要等待队列都处理完才出发callAsync回调。

    • callAsync 触发监听事件
  2. tapAsync 注册异步果子,全部任务完成后执行最终回调

    • callAsync 触发监听事件
  3. tapPromise 用promise注册异步钩子,全部完成后才算成功

    • promise 触发监听事件
const { AsyncParallelHook } = require('tapable')
let queue = new AsyncParallelHook(["name"]);

// 同步注册钩子,按顺序执行回调,不需要等待队列都处理完才出发callAsync回调。
// 下面的执行答案为:1、2、undefined 错误信息、3
queue.tap("1", function (name) {
  console.log(1);
});
queue.tap("2", function (name) {
  console.log(2);
});
queue.tap("3", function (name) {
  setTimeout(() => {
    console.log(3);
  }, 3000)
});
queue.callAsync("yh", (err) => {
  console.log(err, '错误信息');
});

// 异步注册钩子,需要全部任务完成后才执行callAsync回调
// callback一旦传入参数,就会走触发监听事件的函数,也就是callAsync
// 且一旦走过触发监听事件的函数callAsync,就不会再走了。比如有两个callback有入参,那只会是第一个callback执行的时候去执行callAsync,第二个callback不会触发callAsync了。
queue.tapAsync('1', (name, callback) => {
  setTimeout(() => {
    console.log(1);
    callback(111);
  }, 1000)
})
queue.tapAsync('2', (name, callback) => {
  setTimeout(() => {
    console.log(2);
    callback(222);
  }, 2000)
})
queue.tapAsync('3', (name, callback) => {
  setTimeout(() => {
    console.log(3);
    callback();
  }, 3000)
})
queue.callAsync('yh', err => {
  console.log(err, '错误信息');
})

// tapPromise注册钩子,注册时,必须返回一个promise
// resolve()的参数是没有作用的,reject的参数会传到queue.promise的错误回调中。且一旦触发reject,就会执行promise的错误回调,并不会等到玩不执行完后去执行。当然,执行了reject,也不会阻止事件的继续触发。
queue.tapPromise('1', (name) => {
  return new Promise((resolve, reject) => {
    setTimeout(function () {
      console.log(1);
      resolve(111);
    }, 1000);
  })
})
queue.tapPromise('1', (name) => {
  return new Promise((resolve, reject) => {
    setTimeout(function () {
      console.log(2);
      reject(222);
    }, 2000);
  })
})
queue.tapPromise('1', (name) => {
  return new Promise((resolve, reject) => {
    setTimeout(function () {
      console.log(3);
      resolve();
    }, 3000);
  })
})
queue.promise('yh').then((res) => {
  console.log('成功触发', res)
}, (err) => {
  console.log('错误触发', err)
})
6.AsyncParallelBailHook

AsyncParallelBailHook

  • 带保险的异步并执行钩子

  • 有一个任务返回值不为undefined就结束

  • 对于promise来说,resolve和reject并没有区别

  • 区别在于是否给他们传了参数

该钩子有三种注册监听事件的方式:

  1. tap 注册同步钩子,按顺序执行回调,不需要等待队列都处理完才出发callAsync回调,因为是bailHook,所以有返回值则结束,直接走触发函数callAsync回调

    • callAsync 触发监听事件
  2. tapAsync 注册异步果子,全部任务完成后执行最终回调

    • callAsync 触发监听事件
  3. tapPromise 用promise注册异步钩子,只要有一个任务有 resolve 或者 reject 值,不管成功失败都结束

    • promise 触发监听事件
let { AsyncParallelBailHook } = require("tapable");
let queue = new AsyncParallelBailHook(["name"]);

// tap同步注册,callAsync触发,不需要等待队列都处理完才出发callAsync回调,因为是bailHook,所以有返回值则结束,直接走触发函数callAsync回调
queue.tap("1", function (name) {
  console.log(1);
});
queue.tap("2", function (name) {
  console.log(2);
  return 'aaa';
});
queue.tap("3", function (name) {
  setTimeout(() => {
    console.log(3)
  }, 3000);
});
queue.callAsync("yh", (err) => {
  console.log(err, '错误信息');
});

// tapAsync注册,callAsync触发,任务全部完成才触发CallAsync回调,因为是bailHook,所以有返回值则结束,直接走触发函数callAsync回调
queue.tapAsync("1", function (name, callback) {
  console.log(1);
  callback("Wrong");
});
queue.tapAsync("2", function (name, callback) {
  console.log(2);
  callback();
});
queue.tapAsync("3", function (name, callback) {
  console.log(3);
  callback();
});
queue.callAsync("yh", (err) => {
  console.log(err);
});

//  tapPromise, 只要有一个任务有 resolve 或者 reject 值,不管成功失败都结束,结束的意思是都走callAsync触发函数的回调,并不是说其他注册的函数都不执行了,执行还是要执行的。
//  resolve或reject执行或一次后面的就无效了,无效的意思是不会再出发callAsync函数了
queue.tapPromise("1", function (name) {
  return new Promise(function (resolve, reject) {
    setTimeout(function () {
      console.log(1);
      //对于promise来说,resolve还reject并没有区别
      //区别在于你是否传给它们的参数
      resolve(1);
    }, 1000);
  });
});
queue.tapPromise("2", function (name) {
  return new Promise(function (resolve, reject) {
    setTimeout(function () {
      console.log(2);
      reject(2); // resolve或reject执行或一次后面的就无效了。
    }, 2000);
  });
});

queue.tapPromise("3", function (name) {
  return new Promise(function (resolve, reject) {
    setTimeout(function () {
      console.log(3);
      resolve();
    }, 3000);
  });
});

queue.promise("yh").then(
  (result) => {
    console.log("成功", result);
  },
  (err) => {
    console.error("失败", err);
  }
);
7.AsyncSeriesHook

AsyncSeriesHook

  • 异步穿行钩子

  • 任务一个一个执行,执行完上一个执行下一个

// tap同步注册,callAsync触发,不需要等待队列都处理完才出发callAsync回调,
queue.tap("1", function (name) {
  console.log(1);
});
queue.tap("2", function (name) {
  console.log(2);
});
queue.tap("3", function (name) {
  setTimeout(() => {
    console.log(3);
  })
});
queue.callAsync("yh", (err) => {
  console.log(err);
});

// tapAsync异步注册钩子,一个执行完了才会执行下一个,前提是调用callback(),如果没有callback(),那么就结束掉了,连callAsync都不会触发
queue.tapAsync("1", function (name, callback) {
  setTimeout(function () {
    console.log(1);
    callback();
  }, 1000);
});
queue.tapAsync("2", function (name, callback) {
  setTimeout(function () {
    console.log(2);
  }, 2000);
});
queue.tapAsync("3", function (name, callback) {
  setTimeout(function () {
    console.log(3);
    callback();
  }, 3000);
});
queue.callAsync("yh", (err) => {
  console.log(err, '错误信息');
});

// tapPromise注册监听,需要返回一个promise,一个promise执行完才执行下一个。resolve的参数是无效的,resolve也不能阻止允许,但是reject调用会阻止允许,直接进入queue.promise的失败回调,参数有效。
queue.tapPromise("1", function (name) {
  return new Promise(function (resolve) {
    setTimeout(function () {
      console.log(1, name);
      resolve();
    }, 1000);
  });
});
queue.tapPromise("2", function (name) {
  return new Promise(function (resolve, reject) {
    setTimeout(function () {
      console.log(2, name);
      reject('e');
    }, 2000);
  });
});
queue.tapPromise("3", function (name) {
  return new Promise(function (resolve) {
    setTimeout(function () {
      console.log(3, name);
      resolve();
    }, 3000);
  });
});
queue.promise("yh").then((data) => {
  console.log(data);
}, (err) => {
  console.log(err);
});
8.AsyncSeriesBailHook

AsyncSeriesBailHook

  • 只要有一个返回了不为 undefined 的值就直接结束
let { AsyncSeriesBailHook } = require("tapable");
let queue = new AsyncSeriesBailHook(["name"]);

// tap
queue.tap("1", function (name) {
  console.log(1);
  return "Wrong";
});
queue.tap("2", function (name) {
  console.log(2);
});
queue.tap("3", function (name) {
  console.log(3);
});
queue.callAsync("yh", (err) => {
  console.log(err);
});

//tapAsync
queue.tapAsync("1", function (name, callback) {
  setTimeout(function () {
    console.log(1);
    callback("wrong");
  }, 1000);
});
queue.tapAsync("2", function (name, callback) {
  setTimeout(function () {
    console.log(2);
    callback();
  }, 2000);
});
queue.tapAsync("3", function (name, callback) {
  setTimeout(function () {
    console.log(3);
    callback();
  }, 3000);
});
queue.callAsync("yh", (err) => {
  console.log(err);
});

// tapPromise
queue.tapPromise("1", function (name) {
  return new Promise(function (resolve) {
    setTimeout(function () {
      console.log(1);
      resolve();
    }, 1000);
  });
});
queue.tapPromise("2", function (name, callback) {
  return new Promise(function (resolve, reject) {
    setTimeout(function () {
      console.log(2);
      reject("失败了");
    }, 2000);
  });
});
queue.tapPromise("3", function (name, callback) {
  return new Promise(function (resolve) {
    setTimeout(function () {
      console.log(3);
      resolve();
    }, 3000);
  });
});
queue.promise("yh").then(
  (data) => {
    console.log(data);
    console.timeEnd("cost");
  },
  (error) => {
    console.log(error);
  }
);
9.AsyncSeriesWaterfallHook

AsyncSeriesWaterfallHook

  • 只要有一个返回了不为 undefined 的值就直接结束
let { AsyncSeriesWaterfallHook } = require("tapable");
let queue = new AsyncSeriesWaterfallHook(["name", "age"]);

// tap
queue.tap("1", function (name, age) {
  console.log(1, name, age);
  return "return1";
});
queue.tap("2", function (data, age) {
  console.log(2, data, age);
  return "return2";
});
queue.tap("3", function (data, age) {
  console.log(3, data, age);
});
queue.callAsync("yh", 10, (err) => {
  console.log(err);
});

//tapAsync
queue.tapAsync("1", function (name, age, callback) {
  setTimeout(function () {
    console.log(1, name, age);
    callback(null, 1);
  }, 1000);
});
queue.tapAsync("2", function (data, age, callback) {
  setTimeout(function () {
    console.log(2, data, age);
    callback(null, 2);
  }, 2000);
});
queue.tapAsync("3", function (data, age, callback) {
  setTimeout(function () {
    console.log(3, data, age);
    callback(null, 3);
  }, 3000);
});
queue.callAsync("yh", 10, (err, data) => {
  console.log(err, data);
});

// tapPromise
queue.tapPromise("1", function (name) {
  return new Promise(function (resolve) {
    setTimeout(function () {
      console.log(name, 1);
      resolve(1);
    }, 1000);
  });
});
queue.tapPromise("2", function (data) {
  return new Promise(function (resolve) {
    setTimeout(function () {
      console.log(data, 2);
      resolve(2);
    }, 2000);
  });
});
queue.tapPromise("3", function (data) {
  return new Promise(function (resolve) {
    setTimeout(function () {
      console.log(data, 3);
      resolve(3);
    }, 3000);
  });
});
queue.promise("yh").then((err) => {
  console.log("err", err);
});

4.实现SyncHook

  1. tapable目录结构
-----myTapable
  |_____index.js
  |_____Hook.js
  |_____SyncHook.js
  |_____HookCodeFactory.js
  1. 实现:

    • index.js

      // index.js
      const SyncHook = require('./SyncHook.js');
      
      module.exports = {
        SyncHook
      }
      
    • Hook.js

      /*
        懒编译:
        这个CALL_DELEGATE这样设计比较精妙,因为会将this.call重新赋值,这种就属于懒编译,有两个特点:
        1. 我调用的时候编译,不调用不编译
        2. 调用一次,永久使用。因为调用过一次后,this.call就被重新赋值了,那么下次调this.call的时候,就不需要再走CALL_DELEGATE了,直接走重新赋值的那个内容了
        懒编译举个使用场景:
        答:根据浏览器类型来执行某个逻辑的时候,比如:
        let doSomething = (browser) => {
          if ('IE') {
            doSomething = () => {...}
          }else if ('Chrome') {
            doSomething = () => {...}
          }else if ('FireFox') {
            doSomething = () => {...}
          }
        }
        doSomething('Chrome');
        doSomething();
        doSomething();
      */
      
      const CALL_DELEGATE = function(...args){
        // 先动态创建一个sync同步的类型的call方法,然后赋值给this.call
        // 关于this的问题,只有一句话记住就可以了——谁调用它就指向谁。这里指向钩子的实例,也就是syncHook.call(),所以this指向syncHook
        this.call = this._createCall('sync');
        return this.call(...args);
      }
      class Hook{
        constructor(args = []){
          this.args = args; //['name', 'age']
          this.taps = []; // 存放注册的事件,也就是订阅的函数放在这个数组中,里面放了[{type, name, fn}], 注意下面的this._x
          this.call = CALL_DELEGATE;
          this._x = undefined; // 这其实才是真正存放我们时间函数的数组,里面放了[fn],注意上面的this.taps。this.taps.map(tap => tap.fn) => this._x
        }
        tap(options, fn){
          this._tap('sync', options, fn)
        }
        _tap(type, options, fn){
          if (typeof options === 'string') {
            options = {name: options};
          }
          // 创建tapInfo,并且插入到this.taps数组中去
          let tapInfo = {...options, type, fn};
          this._insert(tapInfo);
        }
        _resetCompilation(){
          this.call = CALL_DELEGATE;
        }
        _insert(tapInfo){
          this._resetCompilation(); // 每次插入新的函数之前,需要重新编译call方法。因为编译过一次之后CALL_DELEGATE中的_createCall会动态创建一个函数,这个函数编译了x个tap函数,这时候新增了一个,那就是x+1个函数了,不再使用原来的x个的了,所以需要重新编译出x+1个tap的函数出来,所以需要重写
          this.taps.push(tapInfo);
        }
        compile(options){
          throw new Error('Abstract:此方法应该被子类重写')
        }
        _createCall(type){
          // 动态创建一个函数
          return this.compile({
            taps: this.taps, // 要执行的函数
            args: this.args, // 事件函数接收的参数
            type // 执行的类型 sync
          })
        }
      }
      
      module.exports = Hook;
      
    • HookCodeFactory

      class HookCodeFactory{
      
        setup(hookInstance, options){
          // 把tapInfo中的fn取出来变成数组赋值给hookInstance._x
          hookInstance._x = options.taps.map(tapInfo => tapInfo.fn);
        }
        init(options){
          this.options = options;
        }
        deInit(){
          this.options = null;
        }
        args(options){
          let allArgs = options.args || []; // ['name', 'age']
          return allArgs.join(','); // 'name,age'
      
        }
        header(){ // new Function()的函数体内头和内容两部分,头是固定的,为`var _x = this._x;`,内容根据实际来定
          let code = ``;
          code += `var _x = this._x;\n`;
          return code;
        }
        callTapsSeries(options){
          if (options.taps.length === 0) {
            return ''; // 如果没有注册过任何的事件函数,内容就是空的
          }
          let code = ``;
          for(let i = 0; i < options.taps.length; i++) {
            let content = this.callTap(i, options);
            code += content;
          }
          return code;
        }
        callTap(tapIndex, options) { // 这里就是构造函数体的内容部分
          // 这个方法要返回var _fn0 = _x[0];_fn0(name, age);
          let code = ``;
          code += `var _fn${tapIndex} = _x[${tapIndex}];\n`
          let tapInfo = options.taps[tapIndex]; //{name, fn, type}
          switch(tapInfo.type) {
            case 'sync':
              code += `_fn${tapIndex}(${this.args(options)});\n`;
              break;
            default:
              break;
          }
          this.deInit();
          return code
        }
        create(options){
          this.init(options);
          let fn;
          switch(this.options.type) {
            case 'sync':
              fn = new Function(
                this.args(options),// 'name,age'
                this.header() + this.content(options), //函数体
              );
              break;
            default:
              break;
          }
          return fn;
        }
      }
      
      module.exports = HookCodeFactory;
      
    • SyncHook.js

      const Hook = require('./Hook')
      const HookCodeFactory = require('./HookCodeFactory');
      class SyncHookCodeFactory extends HookCodeFactory{
        content(options){
          // 穿行调用taps函数,也就是拿出fn0执行,拿出fn1执行,拿出fn2执行...
          return this.callTapsSeries(options);
        }
      }
      let factory = new SyncHookCodeFactory();
      class SyncHook extends Hook{
        compile(options) {
          // 把钩子的实例和选项的值用来初始化代码工厂
          factory.setup(this, options); // options = {type: 'sync', taps, args}
          // 根据选项创建call方法,实现:new Function(args, 函数里(里面会调用taps))
          return factory.create(options);
        }
      }
      
      module.exports = SyncHook;
      

总结:

实现一个SyncHook,主要是实现Hook类,这个Hook提供了tap注册事件的方法,其实就是将时间放到一个队列中,然后通过call方法去触发事件队列。如果我们自己写,其实可以直接遍历这个队列,然后一个个循环执行即可。在tapable中,其实是通过compile方法动态创建一个函数得到的,这个complier是通过集成HookCodeFactory类得到的。那么怎么动态创建,其实就是new Function(),然后第一个参数是函数参数组成的字符串,第二个参数是函数体字符串,那么函数体其实是有两部分组成的,第一部分是固定的,内容为var _x = this._x;\n,第二部分的内容字符串,其实就是每个函数的调用,并传入参数,构成的字符串。这样new出来的Function去执行即可。

最后最好简单的一句话概括下:本质就是将触发的函数放到一个数组中,然后一个个触发,这也就是订阅发布的模式。
**

  • 问:为什么compiler是动态new一个Function,而不是直接去遍历taps?

    因为这个compiler是统一的编译方法,适合九大Hook,每个Hook的对taps的函数执行的逻辑都不一样,比较灵活和复杂,所以不再为每个hook为写一套遍历taps的逻辑,而是统一写一个new Function,传入参数字符串和函数体字符串即可。

5.实现AsyncParallelHook异步并行——tapAsync

原理:这个跟上面的区别在于compiler函数动态new一个Function的函数体不同,函数体如下图所示,即:等到_counter为0的时候调用回调函数,这个原理跟promise.all差不多。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-xaSkHKov-1628583127242)(C:\Users\yuhua7\AppData\Roaming\Typora\typora-user-images\image-20210810110346630.png)]

6.实现AsyncParallelHook异步并行——promise

原理:这个跟上面的区别在于compiler函数动态new一个Function的函数体不同,函数体如下图所示,即:等到_counter为0的时候调用回调函数,这个原理跟promise.all差不多。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-pcHIx1o3-1628583127246)(C:\Users\yuhua7\AppData\Roaming\Typora\typora-user-images\image-20210810125146461.png)]

7.实现AsyncSeriesHook异步串行——tapAsync

原理:这个跟上面的区别在于compiler函数动态new一个Function的函数体不同,函数体如下图所示,其实就是从callAsync开始一层套一层

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-1uKoWfBX-1628583127252)(C:\Users\yuhua7\AppData\Roaming\Typora\typora-user-images\image-20210810143113346.png)]

8.hook的拦截器interceptor

8.1 拦截器的三大API

tapable的每个hook都提供了拦截器interceptor,该拦截器主要提供常用的三大API为:

  • register
    • 当注册一个新的事件函数的视乎,会触发register函数
  • tap
    • 每个事件函数执行的时候(不是注册的时候,是触发的时候),会触发tap函数
  • call
    • 每一个call的触发会触发一次该拦截器
8.2拦截器的使用

首先,父类Hook类提供了intercept方法,参数为interceptor,执行该方法就相当于注册了一个拦截器。

另外,每个hook可以注册多个拦截器,比如下面就数注册了两个拦截器:

const { SyncHook } = require('./myTapable/index');
const syncHook = new SyncHook(['name', 'age']);
let interceptor1 = {
  // 当你注册一个新的事件函数的额时候,会触发register
  register(tapInfo){
    console.log('拦截器1开始register', tapInfo); // tapInfo的内容就是当前注册函数需要保存在taps队列中的信息,有type,fn和name三个属性,比如:{ type: 'sync', fn: [Function], name: '事件函数1' }
    tapInfo.flag = 'uuu'; // 做个标志,看看interceptor2的register的参数tapInfo会不会有flag
    return tapInfo;
  },
  // tap拦截器,是在每个事件函数触发的时候(不是注册的时候,是触发的时候)执行,准确的说是每个事件触发之前
  tap(tapInfo){
    console.log('拦截器1开始tap', tapInfo);
  },
  call(name, age){ // 每一个call会触发一次call拦截器,准确得说是每个call触发之前
    console.log('拦截器1开始call', name, age);
  }
};
let interceptor2 = {
  register(tapInfo){ // 这里的tapInfo,是来自于上一个interceptor的register的,所以这个tapInfo会有上面的flag标志
    console.log('拦截器2开始register', tapInfo);
    return tapInfo;
  },
  tap(tapInfo){
    console.log('拦截器2开始tap', tapInfo);
  },
  call(name, age){ // 每一个call会触发一次call拦截器,准确得说是每个call触发之前
    console.log('拦截器2开始call', name, age);
  }
};
syncHook.intercept(interceptor1)
syncHook.intercept(interceptor2) // interceptor可以多个,那么每次注册新的事件,也就是tap的时候,会将所有interceptor的register执行一遍

源码中,会把每个拦截器放在一个数组中,每次触发拦截器API的时候,会遍历该数组。所以,如上面的例子,注册一个时间函数,会触发两次register,触发一个事件,会触发两次tap,触发一次call,会触发两次拦截器的call

8.3拦截器API的参数

拦截器API的参数(默认有一个),

  • 一般为当前注册函数需要保存在taps队列中的信息,有type,fn和name三个属性,比如:{ type: ‘sync’, fn: [Function], name: ‘事件函数1’ }
  • 如果一个拦截器的钩子有返回值,会进入下一个拦截器同类钩子的第一个参数

9.HookMap

HookMap的实例是map,可以用map来批量创建钩子

  • 举例:
let { SyncHook, HookMap } = require('tapable');
const map = new HookMap(() =>  new SyncHook(['name']));

// map是用来批量创建钩子的 key是字符串 值是一个钩子hook,就是上面第二行new HookMap()的参数执行的结果。
map.for('key1').tap('plugin1', (name) => console.log(1, name));
map.for('key1').tap('plugin2', (name) => console.log(2, name));
map.get('key1').call('yh');

map.for('key2').tap('plugin3', (age) => console.log(3, age));
map.for('key2').tap('plugin3', (age) => console.log(4, age));
map.get('key2').call(18);
  • map是用来批量创建钩子的 key是字符串 值是一个钩子hook,就是上面第二行new HookMap()的参数执行的结果。
9.1HookMap的实现
class HookMap {
  constructor(factory) {
    this._map = new Map();
    this._factory = factory;
  }
  get(key) {
    return this._map.get(key);
  }
  tapAsync(key, options, fn) {
    return this.for(key).tapAsync(options, fn);
  }
  tapPromise(key, options, fn) {
    return this.for(key).tapPromise(options, fn);
  }
  for(key) {
    const hook = this.get(key);
    if (hook) return hook;
    let newHook = this._factory();
    this._map.set(key, newHook);
    return newHook;
  }
}
module.exports = HookMap;

10.stage

每个钩子在注册事件函数的时候,也就是调用tap的时候,第一个参数有以下几种情况:

  • 可以是字符串,比如字符串’xxx’,那么源码会转成{name: ‘xxx’}
  • 可以是对象{name: ‘xxx’},如果是对象,还可以加一个stage属性,值为数字类型,比如{name: ‘xxx’, stage: 2}

这个stage的大小,代表该tap回调触发的顺序,越小的越先触发。

  • 使用
let {SyncHook} = require('tapable');
let hook = new SyncHook(['name']);
hook.tap({name:'tap1',stage:1},(name)=>{
   console.log(1,name);
});
hook.tap({name:'tap3',stage:3},(name)=>{
   console.log(3,name);
});
hook.tap({name:'tap5',stage:5},(name)=>{
   console.log(4,name);
});
hook.tap({name:'tap2',stage:2},(name)=>{
   console.log(2,name);
});

hook.call('yh'); // 按照stage的值大小为先后顺序来执行
  • 实现:一个插入排序,即:每注册一个事件函数的时候,都会根据stage的大小,在已有的taps队列中进行插入。

                let stage = 0;
                if (typeof tapInfo.stage === "number") {
                    stage = tapInfo.stage;
                }
                let i = this.taps.length;
                while (i > 0) {
                    i--;
                    const x = this.taps[i];
                    this.taps[i + 1] = x;
                    const xStage = x.stage || 0;
                    if (xStage > stage) {
                        continue;
                    }
                    i++;
                    break;
                }
                this.taps[i] = tapInfo;
    

11.before

指定tap在哪些tap之前执行

  • 使用:
let {SyncHook} = require('tapable');
let hook = new SyncHook(['name']);
hook.tap({name:'tap1'},(name)=>{
   console.log(1,name);
});
hook.tap({name:'tap3'},(name)=>{
   console.log(3,name);
});
hook.tap({name:'tap5'},(name)=>{
   console.log(4,name);
});
hook.tap({name:'tap2',before:['tap3','tap5']},(name)=>{
   console.log(2,name);
});

hook.call('yh');
  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值