最新公告
  • 欢迎您光临起源地模板网,本站秉承服务宗旨 履行“站长”责任,销售只是起点 服务永无止境!立即加入钻石VIP
  • 解析tapable并实现一个自己的tapable

    正文概述 掘金(evanoooo)   2021-02-01   840

    tapable简介

    一个类似EventListener,但功能更强大库,用于发布订阅事件
    它是webpack内部的一个库,用于实现webpack的plugin体系
    github地址:官方tapable(当前v2.2.0)、自己的tapable

    方法

    方法执行方式备注
    SyncHook同步串行-SyncBailHook同步串行返回值不是undefined,跳过后续tap执行并返回SyncWaterfallHook同步串行上一个tap返回值传给下一个tapSyncLoopHook同步循环返回值不是undefined,循环。多个taps时返回不是undefined,从第一个tap重新循环AsyncParallelHook异步并发-AsyncParallelBailHook异步并发返回值不是undefined,跳过后续tap执行并返回AsyncSeriesHook异步串行-AsyncSeriesLoopHook异步循环返回值不是undefined,循环。多个taps时返回不是undefined,从第一个tap重新循环AsyncSeriesBailHook异步串行返回值不是undefined,跳过后续tap执行并返回AsyncSeriesWaterfallHook异步串行上一个tap返回值传给下一个tap

    如上表格,tapable支持若干方法,主要区别为:

    • 同步(Sync) / 异步(Async)
    • 串行(Series) / 并发(Parallel)
    • 上一个的输出做下一个的输入(Waterfall) / tap返回值异常停止后续执行(Bail) / 不关心返回值

    拦截器:包含四个方法

    • register:注册时触发,可以修改tap函数
    • call:执行call方法时触发的钩子
    • tap:执行每个tap时触发的钩子
    • loop:执行循环时触发的钩子

    安装

    yarn add tapable  # 官方的
    yarn add @copyist/tapable  # 自己写的
    

    使用

    • 同步执行
    const hook = new SyncHook(['arg1']);
    hook.tap('tap1', (arg1) => {
      console.log('tap1执行:', arg1);
    })
    hook.tap('tap2', (arg1) => {
      console.log('tap2执行:', arg1);
    })
    hook.call('1');
    // 输出:
    // tap1执行: 1
    // tap2执行: 1
    
    • 异步执行
    const hook = new AsyncSeriesHook(['arg1']);
    hook.tapAsync('tap1', (arg1, cb) => {
      console.log('tap执行:', arg1);
      cb();
    })
    hook.callAsync('1', () => {
      console.log('done')
    });
    hook.promise('2').then(() => {
      console.log('promise done')
    })
    // 输出:
    // tap执行: 1
    // done
    // tap执行: 2
    // promise done
    

    解析:实现两个基类Hook、HookCodeFactory

    下面根据tapable的功能进行实现,代码思路基本相同但不会完全相同
    不熟悉api的同学可以先看看api

    Hook:注册tap事件、intercept拦截器

    1、实现tap注册,tap队列有优先级,可以插队

    • 用stage字段表示优先级,默认为0,越小越优先
    • 用before字段表示插入到哪个前面

    因为插入是按序的,所以调整顺序时使用插入排序

    // 实现tap方法,并用stage表示优先级
    class Hook {
      constructor() {
        this.taps = [];
      }
    
      /** 处理参数*/
      _dealOption(option){
        if(typeof option === 'string') {
          option = {
            name: option,
          }
        }
        return option;
      }
      /* 插入tap*/
      tap(option, fn){
        option = this._dealOption(option);
        option.fn = fn;
        const stage = option.stage || 0;
       
        let i = this.taps.length;
        /** 插入排序,stage字段判断是否向前 */
        while (i > 0) {
          const tap = this.taps[i - 1];
          this.taps[i] = tap;
          const tState = tap.stage || 0;
          /** stage小就往前排 */
          if (stage < tState) {
            i--;
            continue;
          }
          break;
        }
        this.taps[i] = option;
      }
    }
    

    测试

    const hook = new Hook();
    hook.tap('a',()=>{});
    hook.tap({name:'b',stage: -1},()=>{})
    console.log(hook.taps)
    // 输出:
    // {name: 'b', stage: -1, fn: [Function]}
    // {name: 'a', fn: [Function]}
    
    // 修改tap方法,添加before插队
    tap(option, fn){
      option = this._dealOption(option);
      option.fn = fn;
      const stage = option.stage || 0;
      let before = new Set();
      if (typeof option.before === 'string') {
        before = new Set([option.before]);
      } else if (Array.isArray(option.before)) {
        before = new Set(option.before);
      }
      let i = this.taps.length;
      /** 插入排序,stage和before字段判断是否向前 */
      while (i > 0) {
        const tap = this.taps[i - 1];
        this.taps[i] = tap;
        const tState = tap.stage || 0;
        if (stage < tState) {
          i--;
          continue;
        }
        if (before.size) {
          before.delete(tap.name);
          i--;
          continue;
        }
        break;
      }
      this.taps[i] = option;
    }
    

    测试

    const hook = new Hook();
    hook.tap('a',()=>{});
    hook.tap({name:'b',before: ['a']},()=>{})
    console.log(hook.taps)
    // 输出:
    // {name: 'b', before: ['a'], fn: [Function]}
    // {name: 'a', fn: [Function]}
    

    2、实现interceptor注册
    register:注册时触发,可以修改tap函数

    // 添加intercept方法
    intercept(option){
      // 构造函数需添加 this.interceptors = [];
      this.interceptors.push(option);
      if (option.register) {
        for (let i = 0; i < this.taps.length; i++) {
          // 修改tap
          this.taps[i] = this.interceptors[i].register(this.taps[i]);
        }
      }
    }
    

    测试

    const hook = new Hook();
    hook.tap('a',()=>{});
    hook.intercept({
      register: tap => {
        tap.fn = () => {
          console.log('修改过的tap..')
        }
        return tap;
      }
    })
    console.log(hook.taps[0].fn());
    // 输出:修改过的tap..
    

    interceptor中的其他三个方法:call、tap和loop是执行时的钩子,是HookCodeFactory中的一部分

    HookCodeFactory:构造可执行的代码

    实现call、callAsync和promise等方法,生成一个Function再执行

    看Hook类里面的代码

    class Hook{
      /** _createCall得到一个Function */
      call = () => {
        this.call = hookCodeFactory._createCall("sync");
        return this.call(...args);
      };
    }
    

    1、第一次调用call方法时,会根据参数生成一个new Function
    2、再次调用call方法,不用再次生成而是直接拿上次的Function执行。
    3、为什么这样可以翻到下面的[tapable相关问题]

    _createCall中包含tap函数和拦截器函数的执行,思路如下:
    1、定义好各个变量
    2、执行call的钩子函数
    3、分别执行taps,先执行tap钩子函数,再执行tap

    class HookCodeFactory {
      _createCall(type = 'sync'){
        // 1、定义变量
        let code = `
          var _taps = this.taps;
          var _x = _taps.map(tap => tap.fn);
          var _interceptors = this.interceptors;
        `
        // 2、执行拦截器call钩子函数
        // code += interceptorsCall();
      
        const _interceptorsTaps = this.interceptors.map(_ => _.tap).filter(Boolean);
      
        // 3、依次执行taps,先执行tap钩子再执行tap
        code += `
          const _interceptorsTaps = _interceptors.map(_ => _.tap).filter(Boolean);
          _taps.forEach((tap, tIdx) => {
            ${_interceptorsTaps.map((_tap, idx) => `_interceptorsTaps[${idx}](tap)`).join(';')}
            _x[tIdx]();
          })
        `;
        return new Function('', code);
      }
    }
    

    测试

    const hook = new Hook();
    hook.tap('a', () => { console.log('执行...') });
    hook.call()
    // 输出:执行...
    

    可以通过下面方法看到生成的Function代码

    const hook = new Hook();
    hook.tap('a', () => {});
    hook.call();
    console.log(hook.call.toString());  // 输出生成的call函数
    

    实现各种Hook

    上面的例子,实现了一个简单的同步Hook,但要实现不同的Hook,需要改造,如图
    解析tapable并实现一个自己的tapable

    • 将公共方法放在基类,实现类的不同方法在继承时重写

    注册事件的tap、tapAsync和tapPromise方法

    tap是一个事件,自然就有同步和异步之分,所以有这三个注册方法
    不是所有Hook都有这三种注册方式,同步的Hook不能注册异步tap,因为同步Hook需要同步执行完事件

    // 同步事件
    hook.tap('sync', () => {
      console.log('注册sync tap')
    });
    // async事件
    hook.tapAsync('async', (cb) => {
      console.log('注册async tap');
      cb();
    });
    // tapPromise事件
    hook.tapPromise('tapPromise', () => {
      return new Promise((resolve) => {
        console.log('注册promise tap');
        resolve();
      })
    });
    

    执行事件的call、callAsync、promise

    执行事件也同步和异步,所以也有三种
    不是所有Hook都有这三种执行方式,异步Hook不能调用call,因为注册了异步的事件,执行也肯定需要异步完成。

    // 同步执行
    hook.call();
    // async执行
    hook.callAsync('async', () => {
      console.log('async执行完的回调');
    });
    // promise执行
    hook.promise('promise').then(() => {
      console.log('promise执行完事件')
    });
    

    举例1、SyncBailHook

    方法特性:同步串行,tap返回值不是undefined则跳过后续tap执行并返回
    注册用的是tap,不能用tapAsync或tapPromise
    执行事件可以是call、callAsync或promise
    要实现的功能:

    hook.tap('tap1', () => {
      console.log('tap1...');
    });
    hook.intercept({
      tap: () => {},
      call: () => {},
    });
    hook.call();
    

    需要执行的代码:

    • 调用拦截器的call方法
    • 调用拦截器的tap方法
    • 调用同步的tap方法

    构造的函数大致如下:

    function anonymous(){
      // 1、定义变量
      var _taps = this.taps;
      var _x = _taps.map(tap => tap.fn);
      var _interceptors = this.interceptors;
      // 2、执行call钩子函数
      _interceptors.forEach(interceptor => {
        if (interceptor.call) {
          interceptor.call();
        }
      })
      // 3、依次执行taps
      const _interceptorsTaps = this.interceptors.map(_ => _.tap).filter(Boolean);
      let result;
      for(let i=0;i<_taps.length;i++){
        _interceptorsTaps.map((_interceptorsTap, idx) => {
          // 3.1 先执行tap拦截器函数
            _interceptorsTaps[idx](tap);
          }
          // 3.2 再执行tap函数
          result = _x[tIdx]();
          if (result !== undefined) {
            // 4、tap返回值不是undefined,不再往下执行
            return;
          }
      }
    }
    

    1、如果将hook.call()换成hook.callAsync(()=>{})

    • anonymous函数添加一个callback参数,执行时传入
    callAsync(...args){
      this.callAsync = hookCodeFactory._createCall();
      this.callAsync(...args); // 将参数传入
    }
    
    • 上面的第4点return改为
    if (result !== undefined) {
      callback();
    }
    

    2、如果将hook.call()换成hook.promise().then(...)

    • 执行taps的部分要变成return new Promise()
    • 上面的第4点return改为
    if (result !== undefined) {
      resolve();
    }
    

    举例2、AsyncSeriesHook

    异步串行
    注册用的是tap、tapAsync或tapPromise
    执行事件可以是callAsync或promise,不能用call
    要实现的功能:

    hook.tapAsync('tap1', (cb) => {
      console.log('tap1...');
      cb();
    });
    hook.tapAsync('tap2', (cb) => {
      console.log('tap2...');
      cb();
    });
    hook.intercept({
      tap: () => {},
      call: () => {},
    })
    hook.callAsync();
    

    需要执行的代码:

    • 调用拦截器的call方法
    • 调用拦截器的tap方法
    • 调用异步的tap方法

    需要依次执行每个异步方法,所以将每个tap用函数包裹,一个执行完再执行下一个
    有点类似于中间件
    构造的函数大致如下:

    function anonymous(callback){
      // 1、定义变量
      var _taps = this.taps;
      var _x = _taps.map(tap => tap.fn);
      var _interceptors = this.interceptors;
      // 2、执行call钩子函数
      _interceptors.forEach(interceptor => {
        if (interceptor.call) {
          interceptor.call();
        }
      })
      // 3、依次执行taps
      const _interceptorsTaps = this.interceptors.map(_ => _.tap).filter(Boolean);
      function next0(){
        function done(){
          typeof next1 !== 'undefined' ? next1() : callback();
        }
        // 3.1 先执行tap拦截器函数
          _interceptorsTaps[0](tap);
        // 3.2 再执行tap函数
        _x[0](done);
      };
      function next1(){
        function done(){
          typeof next2 !== 'undefined' ? next2() : callback();
        }
        // 3.1 先执行tap拦截器函数
          _interceptorsTaps[1](tap);
        // 3.2 再执行tap函数
        _x[1](done);
      }
      next0();
    }
    

    1、如果将tapAsync改为tapPromise,则

    • 3.2执行tap函数改为 _x[0]().then(() => done());
    • callback()部分改成resolve()

    举例3、AsyncParallelHook

    异步并发
    注册用的是tap、tapAsync或tapPromise
    执行事件可以是callAsync或promise,不能用call
    要实现的功能:

    hook.tapAsync('tap1', (cb) => {
      console.log('tap1...');
      cb();
    });
    hook.tapPromise('tap2', () => {
      return Promise(resolve=>{
        console.log('tap2...');
        resolve();
      })
    });
    hook.intercept({
      tap: () => {},
      call: () => {},
    });
    await hook.promise();
    

    需要执行的代码:

    • 调用拦截器的call方法
    • 调用拦截器的tap方法
    • 调用异步的tap方法
    • 统计是否执行完,是则完成
    function anonymous(callback){
      // 1、定义变量
      var _taps = this.taps;
      var _x = _taps.map(tap => tap.fn);
      var _interceptors = this.interceptors;
      // 2、执行call钩子函数
      _interceptors.forEach(interceptor => {
        if (interceptor.call) {
          interceptor.call();
        }
      })
      // 3、依次执行taps
      const _interceptorsTaps = this.interceptors.map(_ => _.tap).filter(Boolean);
      // 需要执行的tap数量
      let count = _x.length;
      function done(){
        if (--count <= 0) {
          resolve();
        }
      }
      // 3.1 先执行tap拦截器函数
      _interceptorsTaps[0](tap);
      // 3.2 再执行tap函数
      _x[0](done);
      // 3.1 先执行tap拦截器函数
      _interceptorsTaps[1](tap);
      // 3.2 再执行tap函数
      _x[1]().then(done);
    }
    

    规律

    • 根据tap不同方法,构造不同的tap执行函数。
      • tap方法同步调用
      • tapAsync方法需要传入done函数(判断执行下一个还是结束)
      • tapPromise需要在then中处理done(判断执行下一个还是结束)
    • 根据call不同方法,使用不同函数体。
      • call方法同步调用,依次执行tap
      • callAsync需要依次调用tap,执行完调用callback
      • promise需要返回promise,taps执行完再resolve
    // ①参数
    function anonymous(args){
    // ②变量
    
    // ③拦截器call方法
    
    // ④执行taps主体 
    {
      // ⑤拦截器tap方法
    
      // ⑥执行tap
    
      // ⑦判断tap执行结果
    }
    // ⑧执行完taps
    }
    
    
    步骤callcallAsyncpromise
    -callback参数---返回new Promise-传入done函数then中调用done-判断是否有下一个next函数,没有则callback结束判断是否有下一个next函数,没有则callback结束
    步骤taptapAsynctapPromise
    -调用cb结束tap返回promiseresolve()结束tap

    tapable相关问题

    1、为什么用new Function而不是直接依次执行代码?

    • 执行过call后可以缓存call方法,不必再执行判断逻辑,效率更高
    • 这个库是用于webpack plugin的,plugin需要重复调用
    • issue Why HookCodeFactory generated code
    • issue Query regarding coding paradigm used in this library

    2、constructor中的this.tap = this.tap作用?

    • 性能优化 issue

    3、为什么没有untap移除事件方法

    • 也是性能问题 issue

    总结

    • 自己的库是为了学习而仿照着写的。对webpack的流程及源码还不熟悉,还没看在webpack中的具体使用
    • 自己写的库虽然是仿照的,但具体实现还是有些不一样,前面开始写时基本一样,写到后面看了它的API和测试用例就直接自己写了,所以会有差异
    • 文章是写完后再总结的,代码都是伪代码
    • 没有做异常处理
    • 每个hook都有测试用例,拷贝了部分tapable的测试用例,其中HookTester的部分没有拷贝
    • 文章如果有错,欢迎指出

    参考

    • webpack4.0源码分析之Tapable
    • Webpack tapable 使用研究
    • webpack系列之二Tapable

    起源地下载网 » 解析tapable并实现一个自己的tapable

    常见问题FAQ

    免费下载或者VIP会员专享资源能否直接商用?
    本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
    提示下载完但解压或打开不了?
    最常见的情况是下载不完整: 可对比下载完压缩包的与网盘上的容量,若小于网盘提示的容量则是这个原因。这是浏览器下载的bug,建议用百度网盘软件或迅雷下载。若排除这种情况,可在对应资源底部留言,或 联络我们.。
    找不到素材资源介绍文章里的示例图片?
    对于PPT,KEY,Mockups,APP,网页模版等类型的素材,文章内用于介绍的图片通常并不包含在对应可供下载素材包内。这些相关商业图片需另外购买,且本站不负责(也没有办法)找到出处。 同样地一些字体文件也是这种情况,但部分素材会在素材包内有一份字体下载链接清单。
    模板不会安装或需要功能定制以及二次开发?
    请QQ联系我们

    发表评论

    还没有评论,快来抢沙发吧!

    如需帝国cms功能定制以及二次开发请联系我们

    联系作者

    请选择支付方式

    ×
    迅虎支付宝
    迅虎微信
    支付宝当面付
    余额支付
    ×
    微信扫码支付 0 元