zoukankan      html  css  js  c++  java
  • 从 Tapable 中得到的启发

    Tapable

    Why Tapable

    前端开发中 Webpack 本质上是基于事件流的运行机制,它的工作流程是将特定的任务分发到指定的事件钩子中去完成。而实现这一切的核心就是 tapable,Webpack 中的两个基础模块:负责编译的 Compiler 和负责创建 bundle 的 Compilation 都是 tapable 构造函数的实例。


    在 Webpack 4.0 的源码中会看到下面这些以 Sync、Async 开头,以 Hook 结尾的方法,这些都tapable 核心库的构造类,它为我们提供不同的事件流机制:


    image.png

    • SyncBailHook:同步执行,前一步返回是 undefined 才会进入下一个函数,否则直接结束
    • SyncWaterfallHook:同步执行,前一个函数的执行结果作为下一个函数的参数传入
    • SyncLoopHook:同步执行每个函数,若某个函数返回不为 undefined 则继续循环执行该函数,直至该函数返回 undefined 再进入下一个函数
    • AsyncParallelHook:异步并行执行,知道所有异步函数执行结束再进入最后的 finalCallback
    • AsyncParallelBailHook:异步并行执行,只要监听函数的返回值不为 undefined,就会忽略后面的监听函数执行,直接跳跃到callAsync等触发函数绑定的回调函数,然后执行这个被绑定的回调函数
    • AsyncSeriesHook:异步串行执行,函数参数都来自于最初传入的参数
    • AsyncSeriesBailHook:异步串行执行,只要监听函数的返回值不为 undefined,就会忽略后面的监听函数执行,直接跳跃到callAsync等触发函数绑定的回调函数,然后执行这个被绑定的回调函数
    • AsyncSeriesWaterfallHook:异步串行执行,上一个监听函数的中的callback(err, data)的第二个参数,可以作为下一个监听函数的参数

    Tapable and EventEmitter

    Tapable 和 EventEmitter 都是实现了 事件的订阅与发布 功能,很多刚接触Tapable的同学可能会懵逼,这玩意和 EventEmitter 有什么区别呢?

    • tapable 在创建订阅中心时需要指定回调函数的参数列表
    • tapable 触发事件时不需要指定事件名,所有的事件都会被调用
    // SyncHook 钩子的使用
    const { SyncHook } = require("tapable");
    
    // 创建实例
    let syncHook = new SyncHook(["name"]);
    
    // 注册事件
    syncHook.tap("login", (name) => console.log(name)); // gaollard
    syncHook.tap("register", (name) => console.log(name)); // gaollard
    
    // 触发事件
    syncHook.call("gaollard");
    
    // 引入 events 模块
    const events = require('events');
    
    // 创建 eventEmitter 对象
    const userEvent = new events.EventEmitter();
    
    userEvent.addListener('login', function(name) {
      console.log(name)
    })
    
    userEvent.addListener('register', function(name) {
      console.log(name) // 打印 gaollard
    })
    
    userEvent.emit('login', 'gaollard')
    

    Sync 类型钩子

    • 注册事件 tap
    • 触发事件 call

    SyncHook

    SyncHook 为串行同步执行,什么都不需要关心,在触发事件之后,会按照事件注册的先后顺序执行所有的事件处理函数,参数就是调用call传入的参数:

    // SyncHook 钩子的使用
    const { SyncHook } = require("tapable");
    
    // 创建实例 ["name"] 用于声明回调函数的参数个数
    let userSyncHook = new SyncHook(["name"]);
    
    // 注册事件 第一个参数为事件名, 第二个参数为注册的回调函数
    userSyncHook.tap("login", (name) => console.log(name));
    userSyncHook.tap("register", (name) => console.log(name));
    
    // 触发事件
    userSyncHook.call("gaollard");
    console.log(userSyncHook);
    

    image.png
    在 tapable 解构的 SyncHook 是一个类,注册事件需先创建实例,创建实例时支持传入一个数组,数组内存储事件触发时传入的参数,实例的 tap 方法用于注册事件,支持传入两个参数,第一个参数为事件名称,在 Webpack 中一般用于存储事件对应的插件名称, 第二个参数为事件处理函数,函数参数为执行 call 方法触发事件时所传入的参数的形参。

    SyncBailHook

    SyncBailHook 为串行同步执行,如果事件处理函数执行时有一个返回值不为 undefined,则跳过剩下未执行的事件处理函数:

    // 创建实例
    let userSyncHook = new SyncBailHook(["name"]);
    
    // 注册事件
    userSyncHook.tap("login", (name) => {
      console.log(name)
      return null // 返回值不为 undefined
    });
    
    userSyncHook.tap("register", (name) => {
      console.log(name)
    });
    
    // 触发事件,让监听函数执行
    userSyncHook.call("gaollard");  // 只会打印一次
    

    SyncWaterfallHook

    SyncWaterfallHook 为串行同步执行,上一个事件处理函数的返回值作为参数传递给下一个事件处理函数,依次类推,当然,只有第一个事件处理函数的参数可以通过 call 传递,而 call 的返回值为最后一个事件处理函数的返回值:

    // 创建实例
    let userSyncHook = new SyncWaterfallHook(["name"]);
    
    // 注册事件
    userSyncHook.tap("login", (name) => {
      console.log('login', name) // 打印 gaollard
    });
    
    userSyncHook.tap("register", (name) => {
      console.log('register', name) // login回调未返回值, 所以参数为 "gaollard"
      return "hello"
    });
    
    userSyncHook.tap("enroll", (name) => {
      console.log("enroll", name) // register回调返回"hello", 所以参数为 "hello"
    });
    
    // 触发事件
    userSyncHook.call("gaollard");
    

    image.png

    SyncLoopHook

    SyncLoopHook 为串行同步执行,但是 SyncLoopHook 中的每一个事件回调函数都会被循环执行,事件处理函数返回 undefined 表示结束循环,当前的事件回调循环结束后进入到下一个回调函数中,直到整个流程结束:

    // 创建实例
    let userSyncHook = new SyncLoopHook(["name"]);
    
    let num1 = 1
    
    // 注册事件
    userSyncHook.tap("login", (name) => {
      console.log('login', name, num1)
      return (++num1) > 10 ? undefined : true
    });
    
    userSyncHook.tap("register", (name) => {
      console.log('login', name, num1)
      return (++num1) > 20 ? undefined : true
    });
    
    // 触发事件
    userSyncHook.call("manbax");
    

    卧槽,连 21 也被打印出来了??? 发现了 tapable 一个BUG(写完去github提issue)
    image.png

    Async 类型钩子

    Async 类型可以使用 taptapSynctapPromise 注册不同类型的插件 “钩子”,分别通过 call、callAsync 和 promise 方法调用,我们下面会针对 AsyncParallelHook 和 AsyncSeriesHook 的 async 和 promise 两种方式分别介绍和模拟。

    AsyncParallelHook

    AsyncParallelHook 为异步并行执行,通过 tapAsync 注册的事件,通过 callAsync 触发;通过 tapPromise 注册的事件,通过 promise 触发(返回值可以调用 then 方法)

    • tapAsync/callAsync
    const { AsyncParallelHook } = require("tapable");
    
    // 创建实例
    let asyncParallelHook = new AsyncParallelHook(["name"]);
    
    console.time("time");
    
    // 注册事件
    asyncParallelHook.tapAsync("login", (name, done) => {
      setTimeout(() => {
        console.log("login", name, new Date());
        done();
      }, 1000);
    });
    
    asyncParallelHook.tapAsync("register", (name, done) => {
      setTimeout(() => {
        console.log("register", name, new Date());
        done();
        console.timeEnd("time");
      }, 2000);
    });
    
    // 触发事件, callAsync 的最后一个参数为回调函数,在所有事件处理函数执行完毕后执行。
    asyncParallelHook.callAsync("manbax", () => {
      console.log("complete");
    });
    

    image.png
    上面的代码中:两个事件处理函数会并行的执行,都执行完成后(done 被调用),触发 callAsync 回调函数。所有 tapAsync 注册的事件处理函数最后一个参数都为一个回调函数 done,每个事件处理函数在异步代码执行完毕后调用 done 函数,则可以保证 callAsync 会在所有异步函数都执行完毕后执行,接下来看一看 callAsync 是如何实现的:

    // 模拟 SyncLoopHook 类
    class AsyncParallelHook {
      constructor(args) {
        this.args = args;
        this.taps = [];
      }
      tapAsync(name, task) {
        this.taps.push({
          name: name,
          fn: task,
          type: 'sync',
        });
      }
      callAsync(...args) {
          if (args.length < this.args.length) {
            throw new Error("参数不足");
          }
    
          let sum = 0
          const fn = args.pop();
          const params = args.splice(0, this.args.length);
          const done = () => {
            (++sum === this.taps.length) && fn()
          }
          this.taps.forEach(task => {
            task.fn(params, done)
          })
      }
    }
    
    • tapPromise/promise

    要使用 tapPromise 注册事件,对事件处理函数有一个要求,必须返回一个 Promise 实例,而 promise 方法也返回一个 Promise 实例,callAsync 的回调函数在 promise 方法中用 then 的方式代替:

    const { AsyncParallelHook } = require("tapable");
    
    // 创建实例
    let asyncParallelHook = new AsyncParallelHook(["name"]);
    
    console.time("time");
    
    // 注册事件
    asyncParallelHook.tapPromise("login", (name) => {
      return new Promise((resolve, reject) => {
        setTimeout(() => {
          console.log("login", name, new Date());
          resolve();
        }, 1000);
      })
    });
    
    asyncParallelHook.tapAsync("register", (name) => {
      return new Promise((resolve, reject) => {
        setTimeout(() => {
          console.log("register", name, new Date());
          resolve();
          console.timeEnd("time");
        }, 2000);
      })
    });
    
    // 触发事件
    asyncParallelHook.promise("manbax").then(() => {
      console.log("complete");
    });
    

    image.png
    AsyncParallelHook 的实现:

    class AsyncParallelHook {
      constructor(args) {
        this.args = args;
        this.taps = [];
      }
      tapPromise(name, task) {
        this.taps.push({
          name: name,
          fn: task,
          type: 'async',
        });
      }
      promise(...args) {
          if (args.length < this.args.length) {
            throw new Error("参数不足");
          }
          return new Promise.all(this.taps.map(task => task.fn(...args)))
      }
    }
    

    AsyncParallelBailHook

    • tapPromise/promise
    const { AsyncParallelBailHook } = require("tapable");
    
    // 创建实例
    let userHook = new AsyncParallelBailHook(["name"]);
    
    console.time("time");
    
    // 注册事件
    userHook.tapPromise("login", (name) => {
      return new Promise((resolve, reject) => {
        setTimeout(() => {
          console.log("login", name, new Date());
          resolve(undefined) // 此处为 undefined 进入到下一个回调
        }, 1000);
      })
    });
    
    userHook.tapPromise("register", (name) => {
      return new Promise((resolve, reject) => {
        setTimeout(() => {
          console.log("register", name, new Date());
          resolve("2"); // 这个回调完成后直接触发最后回调
        }, 2000);
      })
    });
    
    userHook.tapPromise("enroll", (name) => {
      return new Promise((resolve, reject) => {
        setTimeout(() => {
          console.log("enroll", name, new Date());
          reject("2");
          console.timeEnd("time");
        }, 3000);
      })
    });
    
    // 触发事件
    userHook.promise("manbax").then((res) => {
      console.log("complete", res)
    }).catch(err => {
      console.log("error", err)
    })
    

    AsyncSeriesHook

    AsyncSeriesHook 为异步串行执行,与 AsyncParallelHook 相同,通过 tapAsync 注册的事件,通过 callAsync 触发,通过 tapPromise 注册的事件,通过 promise 触发,可以调用 then 方法。

    • tapAsync/callAsync
    const { AsyncSeriesHook } = require("tapable");
    
    // 创建实例
    let userHook = new AsyncSeriesHook(["name"]);
    
    console.time()
    
    userHook.tapAsync('login', function(name, done) {
      setTimeout(() => {
        console.log('login--', name, new Date())
        done()
      }, 1000)
    })
    
    userHook.tapAsync('register', function(name, done){
      setTimeout(() => {
        console.log('register--', name, new Date())
        done()
      }, 2000)
    })
    
    // 整个调用花费了 3S
    userHook.callAsync('manbax', () => {
      console.log('complete')
      console.timeEnd()
    })
    
    • tapPromise/promise
    const { AsyncSeriesHook } = require("tapable");
    
    // 创建实例
    let userHook = new AsyncSeriesHook(["name"]);
    
    console.time()
    
    userHook.tapPromise('login', function(name){
      return new Promise((resolve) => {
        setTimeout(() => {
          console.log('login--', name, new Date())
          resolve()
        }, 1000)
      })
    })
    
    userHook.tapPromise('register', function(name){
      return new Promise((resolve) => {
        setTimeout(() => {
          console.log('register--', name, new Date())
          resolve()
        }, 2000)
      })
    })
    
    // 整个调用花费了 3S
    userHook.promise('manbax').then(res => {
      console.log('complete')
      console.timeEnd()
    })
    

    AsyncSeriesBailHook

    const { AsyncSeriesBailHook } = require("tapable");
    
    // 创建实例
    let userHook = new AsyncSeriesBailHook(["name"]);
    
    console.time()
    
    userHook.tapAsync('login', function(name, done) {
      setTimeout(() => {
        console.log('login--', name, new Date())
        done(1) // 这里返回1, 第二个不会执行(register)
      }, 1000)
    })
    
    userHook.tapAsync('register', function(name, done){
      setTimeout(() => {
        console.log('register--', name, new Date())
        done(2)
      }, 2000)
    })
    
    // 整个调用花费了 3S
    userHook.callAsync('manbax', (_, data) => {
      console.log('complete')
      console.timeEnd()
    })
    

    AsyncSeriesWaterfallHook

    • tapAsync/callAsync: tapAsync 中的 done 回调函数需要传入两个参数,第一个表示是否有异常,第二个为返回值。
    const { AsyncSeriesWaterfallHook } = require("tapable");
    
    // 创建实例
    let userHook = new AsyncSeriesWaterfallHook(["name"]);
    
    console.time()
    
    userHook.tapAsync('login', function(name, done) {
      setTimeout(() => {
        console.log('login--', name, new Date())
        done(null, "1")
      }, 1000)
    })
    
    userHook.tapAsync('register', function(name, done){
      setTimeout(() => {
        console.log('register--', name, new Date())
        done(null, "2")
      }, 2000)
    })
    
    // 整个调用花费了 3S
    userHook.callAsync('manbax', (_, data) => {
      console.log('complete', data)
      console.timeEnd()
    })
    

    API模拟实现

    SyncHook

    // 模拟 SyncHook 类
    class MySyncHook {
      constructor(args) {
        this.args = args;
        this.taps = [];
      }
      tap(name, task) {
        this.taps.push({
          name: name,
          fn: task,
          type: 'sync',
        });
      }
      call(...args) {
          if (args.length < this.args.length) {
            // 参数不足时抛出异常
            throw new Error("参数不足");
          }
    
          // 参数长度与创建实例传入数组长度一直,不足补 undefined
          // 因为长度不足时已经抛出异常,故注释
          // args = args.slice(0, this.args.length);
    
          // 依次执行事件处理函数
          this.taps.forEach(task => task.fn(...args));
      }
    }
    

    SyncBailHook

    // 模拟 SyncBailHook 类
    class SyncBailHook {
      constructor(args) {
        this.args = args;
        this.taps = [];
      }
      tap(name, task) {
        this.taps.push({
          name: name,
          fn: task,
          type: 'sync',
        });
      }
      call(...args) {
          if (args.length < this.args.length) {
            throw new Error("参数不足");
          }
          
          let i = 0, res;
          do {
            res = this.taps[i++].fn(...args)
          } while (res === undefined && i < this.taps.length)
      }
    }
    

    SyncWaterfallHook

    // 模拟 SyncWaterfallHook 类
    class SyncWaterfallHook {
      constructor(args) {
        this.args = args;
        this.taps = [];
      }
      tap(name, task) {
        this.taps.push({
          name: name,
          fn: task,
          type: 'sync',
        });
      }
      call(...args) {
          if (args.length < this.args.length) {
            throw new Error("参数不足");
          }
    
          return this.taps.reduce((res, current) => {
            let _res = current.fn(res)
            // 若当前的回调函数没有返回值,那么就使用上一个参数
            return _res !== undefined ? _res : res
          }, ...args)
      }
    }
    

    SyncLoopHook

    // 模拟 SyncLoopHook 类
    class SyncLoopHook {
      constructor(args) {
        this.args = args;
        this.taps = [];
      }
      tap(name, task) {
        this.taps.push({
          name: name,
          fn: task,
          type: 'sync',
        });
      }
      call(...args) {
          if (args.length < this.args.length) {
            throw new Error("参数不足");
          }
    
          let i = 0
          while (i < this.taps.length) {
            const task = this.taps[i++].fn
    
            const magic = function () {
              let res = task(...args)
              if (res !== undefined) {
                magic()
              }
            };
            magic();
          }
      }
    }
    

    AsyncSeriesHook

    class AsyncSeriesHook {
      constructor(args) {
        this.args = args;
        this.taps = [];
      }
      tapAsync(name, task) {
        this.taps.push({
          name: name,
          fn: task,
        });
      }
      callAsync(...args) {
        if (args.length < this.args.length) {
          throw new Error("参数不足");
        }
    
        let i = 0
        const cb = args.pop()
        const _args = args.splice(0, args.length)
    
        const next = () => {
          const task = this.taps[i++]
          if (task) {
            task.fn(..._args, next)
          } else {
            cb()
          }
        }
        next()
      }
    }
    

    AsyncSeriesWaterfallHook

     class AsyncSeriesWaterfallHook {
         constructor() {
             this.tasks = [];
         }
    
         tap(name, task) {
             this.tasks.push(task);
         }
    
         call(...args, finalCb) {
             let count = 0;
             const len = this.tasks.length;
             const next = (err, data) => {
                 if(count === len) return finalCb()
                 let task = this.tasks[count];
                 if (count === 0) {
                     task(...args, next);
                 } else {
                     task(data, next);
                 }
                 count++;
             };
             next()
    
         }
     }
    

    总结

    仔细思考发现 Tapable 事件机制 就像工厂里面生产线:

    • 前序工位的输出是后序工位的输入
    • 当某个产品在流产线上的工位发生异常时,这个产品的后序流程终止


    它非常适合用于解决流水作业,就像 Webpack 对文件进行处理正是这样的场景。学习 tapable 有助于帮助我们更高的理解 Webpack。


    tapable的注册事件的方法有:tab/tapSync/tapPromise 和触发事件的方法 call/callAsync/promise,在 Webpack 中,我们通过这些API来设计钩子,这些 “钩子” 能够将 Webpack 中插件/加载器/功能独立的模块连接起来,以减少耦合性和提高扩展性。

  • 相关阅读:
    localStorage和sessionStorage区别(包括同源的定义)
    跨域问题实践总结! 上(JSONP/document.domain/window.name)
    7月11日计划
    图形验证码知识点整理 Object.prototype.toString.call()等
    学习日报 7-10(验证码)
    Mysql安装与主从配置
    windows service编程
    Entity Framework——常见报错总结
    Entity Framework——读写分离
    Entity Framework——执行sql语句
  • 原文地址:https://www.cnblogs.com/GManba/p/13060030.html
Copyright © 2011-2022 走看看