• home > tools > Bundler > webpackTheory >

    webpack原理(3):Tapable源码分析及钩子函数作用分析

    Author:zhoulujun Date:

    webpack本质上是一种事件流的机制,它的工作流程就是将各个插件串联起来,而实现这一切的核心就是Tapable。这里只是一个学习笔记

    webpack本质上是一种事件流的机制,它的工作流程就是将各个插件串联起来,而实现这一切的核心就是Tapable,webpack中最核心的负责编译的Compiler和负责创建bundles的Compilation都是Tapable的实例。

    Webpack 可以认为是一种基于事件流的编程范例,内部的工作流程都是基于 插件 机制串接起来

    而将这些插件粘合起来的就是webpack自己写的基础类 Tapable 是,plugin方法就是该类暴露出来的;

    基于该类规范而其的 Webpack 体系保证了插件的有序性,使得整个系统非常有弹性,扩展性很好;然而有一个致命的缺点就是调试、看源码真是很痛苦,各种跳来跳去;(基于事件流的写法,和程序语言中的 goto 语句很类似)

    在Tapable1.0之前,也就是webpack3及其以前使用的Tapable,提供了包括

    • plugin(name:string, handler:function)注册插件到Tapable对象中

    • apply(…pluginInstances: (AnyPlugin|function)[])调用插件的定义,将事件监听器注册到Tapable实例注册表中

    • applyPlugins*(name:string, …)多种策略细致地控制事件的触发,包括applyPluginsAsync、applyPluginsParallel等方法实现对事件触发的控制,实现

      1. 多个事件连续顺序执行

      2. 并行执行

      3. 异步执行

      4. 一个接一个地执行插件,前面的输出是后一个插件的输入的瀑布流执行顺序

      5. 在允许时停止执行插件,即某个插件返回了一个undefined的值,即退出执行

    我们可以看到,Tapable就像nodejs中EventEmitter,提供对事件的注册on和触发emit,理解它很重要

    Tapable中的钩子函数

    tapable包暴露出很多钩子类,这些类可以用来为插件创建钩子函数。

    从 https://github.com/webpack/tapable,lib/index.js看出,tapable提供了九种钩子:

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

    所有钩子类的构造函数都接收一个可选的参数,这个参数是一个由字符串参数组成的数组,如下:

    const hook = new SyncHook(["arg1", "arg2", "arg3"]);

    new Hook 新建钩子

    • tapable 暴露出来的都是类方法,new 一个类方法获得我们需要的钩子。

    • class 接受数组参数options,非必传。类方法会根据传参,接受同样数量的参数。

    下面我们就详细介绍一下钩子的用法,以及一些钩子类实现的原理。

    hooks概览

    常用的钩子主要包含以下几种,分为同步和异步,异步又分为并发执行和串行执行,如下图:

    webpack钩子详情

    首先,整体感受下钩子的用法,如下

    钩子名称执行方式使用要点
    SyncHook同步串行不关心监听函数的返回值
    SyncBailHook同步串行只要监听函数中有一个函数的返回值不为 null,则跳过剩下所有的逻辑
    SyncWaterfallHook同步串行上一个监听函数的返回值可以传给下一个监听函数
    SyncLoopHook同步循环当监听函数被触发的时候,如果该监听函数返回true时则这个监听函数会反复执行,如果返回 undefined 则表示退出循环
    AsyncParallelHook异步并发不关心监听函数的返回值
    AsyncParallelBailHook异步并发只要监听函数的返回值不为 null,就会忽略后面的监听函数执行,直接跳跃到callAsync等触发函数绑定的回调函数,然后执行这个被绑定的回调函数
    AsyncSeriesHook异步串行不关系callback()的参数
    AsyncSeriesBailHook异步串行callback()的参数不为null,就会直接执行callAsync等触发函数绑定的回调函数
    AsyncSeriesWaterfallHook异步串行上一个监听函数的中的callback(err, data)的第二个参数,可以作为下一个监听函数的参数

    钩子分为同步VS 异步,细分为  并行VS串行,在根据返回值,细分为不同种类。

    • BasicHook: 执行每一个,不关心函数的返回值,有 SyncHook、AsyncParallelHook、AsyncSeriesHook。

    • BailHook: 顺序执行 Hook,遇到第一个结果 result !== undefined 则返回,不再继续执行。有:SyncBailHook、AsyncSeriseBailHook, AsyncParallelBailHook。

    • WaterfallHook: 类似于 reduce,如果前一个 Hook 函数的结果 result !== undefined,则 result 会作为后一个 Hook 函数的第一个参数。既然是顺序执行,那么就只有 Sync 和 AsyncSeries 类中提供这个Hook:SyncWaterfallHook,AsyncSeriesWaterfallHook

    • LoopHook: 不停的循环执行 Hook,直到所有函数结果 result === undefined。同样的,由于对串行性有依赖,所以只有 SyncLoopHook 和 AsyncSeriseLoopHook (PS:暂时没看到具体使用 Case)

    webpack tabable 钩子类型执行接

    Tabable 关键词解析

    typefunction
    Hook所有钩子的后缀
    Waterfall同步方法,但是它会传值给下一个函数
    Bail熔断:当函数有任何返回值,就会在当前执行函数停止
    Loop监听函数返回true表示继续循环,返回undefine表示结束循环
    Sync同步方法
    AsyncSeries异步串行钩子
    AsyncParallel异步并行执行钩子


    我们可以根据自己的开发需求,选择适合的同步/异步钩子。

    Tapable Hook类


    class Hook {
    	constructor(args) {
    		if(!Array.isArray(args)) args = [];
    		this._args = args; // 实例钩子的时候的string类型的数组
    		this.taps = []; // 消费者
    		this.interceptors = []; // interceptors
    		this.call = this._call =  // 以sync类型方式来调用钩子
    		this._createCompileDelegate("call", "sync");
    		this.promise = 
    		this._promise = // 以promise方式
    		this._createCompileDelegate("promise", "promise");
    		this.callAsync = 
    		this._callAsync = // 以async类型方式来调用
    		this._createCompileDelegate("callAsync", "async");
    		this._x = undefined; // 
    	}
    
    	_createCall(type) {
    		return this.compile({
    			taps: this.taps,
    			interceptors: this.interceptors,
    			args: this._args,
    			type: type
    		});
    	}
    
    	_createCompileDelegate(name, type) {
    		const lazyCompileHook = (...args) => {
    			this[name] = this._createCall(type);
    			return this[name](...args);
    		};
    		return lazyCompileHook;
    	}
    	// 调用tap 类型注册
    	tap(options, fn) {
    		// ...
    		options = Object.assign({ type: "sync", fn: fn }, options);
    		// ...
    		this._insert(options);  // 添加到 this.taps中
    	}
    	// 注册 async类型的钩子
    	tapAsync(options, fn) {
    		// ...
    		options = Object.assign({ type: "async", fn: fn }, options);
    		// ...
    		this._insert(options); // 添加到 this.taps中
    	}
    	注册 promise类型钩子
    	tapPromise(options, fn) {
    		// ...
    		options = Object.assign({ type: "promise", fn: fn }, options);
    		// ...
    		this._insert(options); // 添加到 this.taps中
    	}
    	
    }

    每次都是调用tap、tapSync、tapPromise注册不同类型的插件钩子,通过调用call、callAsync 、promise方式调用。其实调用的时候为了按照一定的执行策略执行,调用compile方法快速编译出一个方法来执行这些插件。

    tabpack提供了同步&异步绑定钩子的方法,并且他们都有绑定事件和执行事件对应的方法。

    Async*Sync*
    绑定:tapAsync/tapPromise/tap绑定:tap
    执行:callAsync/promise执行:call
    call/callAsync 执行绑定事件

    const hook1 = new SyncHook(["arg1", "arg2", "arg3"]);
    
    //绑定事件到webapck事件流
    hook1.tap('hook1', (arg1, arg2, arg3) => console.log(arg1, arg2, arg3)) //1,2,3
    
    //执行绑定的事件
    hook1.call(1,2,3)

    webpack tabable 钩子执行流程

    举个栗子

    • 定义一个Car方法,在内部hooks上新建钩子。分别是同步钩子 accelerate、break(accelerate接受一个参数)、异步钩子calculateRoutes

    • 使用钩子对应的绑定和执行方法

    • calculateRoutes使用tapPromise可以返回一个promise对象。

    //引入tapable
    const {
        SyncHook,
        AsyncParallelHook
    } = require('tapable');
    
    //创建类
    class Car {
        constructor() {
            this.hooks = {
                accelerate: new SyncHook(["newSpeed"]),
                break: new SyncHook(),
                calculateRoutes: new AsyncParallelHook(["source", "target", "routesList"])
            };
        }
    }
    
    const myCar = new Car();
    
    //绑定同步钩子
    myCar.hooks.break.tap("WarningLampPlugin", () => console.log('WarningLampPlugin'));
    
    //绑定同步钩子 并传参
    myCar.hooks.accelerate.tap("LoggerPlugin", newSpeed => console.log(`Accelerating to ${newSpeed}`));
    
    //绑定一个异步Promise钩子
    myCar.hooks.calculateRoutes.tapPromise("calculateRoutes tapPromise", (source, target, routesList, callback) => {
        // return a promise
        return new Promise((resolve,reject)=>{
            setTimeout(()=>{
                console.log(`tapPromise to ${source}${target}${routesList}`)
                resolve();
            },1000)
        })
    });
    
    //执行同步钩子
    myCar.hooks.break.call();
    myCar.hooks.accelerate.call('hello');
    
    console.time('cost');
    
    //执行异步钩子
    myCar.hooks.calculateRoutes.promise('i', 'love', 'tapable').then(() => {
        console.timeEnd('cost');
    }, err => {
        console.error(err);
        console.timeEnd('cost');
    })

    运行结果

    WarningLampPlugin
    Accelerating to hello
    tapPromise to ilovetapable
    cost: 1003.898ms

    calculateRoutes也可以使用tapAsync绑定钩子,注意:此时用callback结束异步回调。

    myCar.hooks.calculateRoutes.tapAsync("calculateRoutes tapAsync", (source, target, routesList, callback) => {
        // return a promise
        setTimeout(() => {
            console.log(`tapAsync to ${source}${target}${routesList}`)
            callback();
        }, 2000)
    });
    
    myCar.hooks.calculateRoutes.callAsync('i', 'like', 'tapable', err => {
        console.timeEnd('cost');
        if(err) console.log(err)
    })


    sync* 钩子

    对于Sync*类型的钩子来说。

    • 注册在该钩子下面的插件的执行顺序都是顺序执行。

    • 只能使用tap注册,不能使用tapPromise和tapAsync注册

    // 所有的钩子都继承于Hook
    class Sync* extends Hook { 
    	tapAsync() { // Sync*类型的钩子不支持tapAsync
    		throw new Error("tapAsync is not supported on a Sync*");
    	}
    	tapPromise() {// Sync*类型的钩子不支持tapPromise
    		throw new Error("tapPromise is not supported on a Sync*");
    	}
    	compile(options) { // 编译代码来按照一定的策略执行Plugin
    		factory.setup(this, options);
    		return factory.create(options);
    	}
    }



    同步串行

    SyncHook

    不关心监听函数的返回值

    SyncHook的用法及实现
    const { SyncHook } = require("tapable");
    let queue = new SyncHook(['name']); //所有的构造函数都接收一个可选的参数,这个参数是一个字符串的数组。
    
    // 订阅-》 注册监听函数
    queue.tap('1', function (name, name2) {// tap 的第一个参数是用来标识订阅的函数的
        console.log(name, name2, 1);
        return '1'
    });
    queue.tap('2', function (name) {
        console.log(name, 2);
    });
    queue.tap('3', function (name) {
        console.log(name, 3);
    });
    
    // 发布
    queue.call('webpack', 'webpack-cli');// 发布的时候触发订阅的函数 同时传入参数
    
    // 执行结果:
    /* 
    webpack undefined 1 // 传入的参数需要和new实例的时候保持一致,否则获取不到多传的参数
    webpack 2
    webpack 3
    */

    通过上面如何使用的案例看出,主要是三个步骤(以同步钩子为例)

    1. new SyncHook(['xx']) 实例化Hook

    2. hook.tap('xxx', () => {}) 注册钩子

    3. hook.call(args) 调用钩子

    原理

    SyncHook是一个很典型的通过发布订阅方式实现的

    class SyncHook_MY{
        constructor(){
            this.hooks = [];
        }
    
        // 订阅
        tap(name, fn){
            this.hooks.push(fn);
        }
    
        // 发布
        call(){
            this.hooks.forEach(hook => hook(...arguments));
        }
    }

    SyncBailHook

    只要监听函数中有一个函数的返回值不为 null,则跳过剩下所有的逻辑

    SyncBailHook的用法及实现

    SyncBailHook为同步串行的执行关系,只要监听函数中有一个函数的返回值不为 null,则跳过剩下所有的逻辑,用法如下:

    const {
        SyncBailHook
    } = require("tapable");
    
    let queue = new SyncBailHook(['name']); 
    
    queue.tap('1', function (name) {
        console.log(name, 1);
    });
    queue.tap('2', function (name) {
        console.log(name, 2);
        return 'wrong'
    });
    queue.tap('3', function (name) {
        console.log(name, 3);
    });
    
    queue.call('webpack');
    
    // 执行结果:
    /* 
    webpack 1
    webpack 2
    */


    原理
    // 钩子是同步的,bail -> 保险
    class SyncBailHook {
      // args => ["name"]
      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);
      }
    }

    SyncWaterfallHook的用法及实现

    上一个监听函数的返回值可以传给下一个监听函数

    SyncWaterfallHook为同步串行的执行关系,上一个监听函数的返回值可以传给下一个监听函数,用法如下:

    const {
        SyncBailHook
    } = require("tapable");
    
    let queue = new SyncBailHook(['name']); 
    
    queue.tap('1', function (name) {
        console.log(name, 1);
    });
    queue.tap('2', function (name) {
        console.log(name, 2);
        return 'wrong'
    });
    queue.tap('3', function (name) {
        console.log(name, 3);
    });
    
    queue.call('webpack');
    
    // 执行结果:
    /* 
    webpack 1
    webpack 2
    */


    SyncWaterfallHook的实现:
    // 钩子是同步的
    class SyncWaterfallHook {
      // args => ["name"]
      constructor() {
        this.tasks = [];
      }
      tap(name, task) {
        this.tasks.push(task);
      }
      call(...args) {
        let [first, ...others] = this.tasks;
        let ret = first(...args);
        others.reduce((a, b) => {
          return b(a);
        }, ret);
      }
    }
    // 简化版
    class SyncBailHook_MY {
        constructor() {
            this.hooks = [];
        }
    
        // 订阅
        tap(name, fn) {
            this.hooks.push(fn);
        }
    
        // 发布
        call() {
            for (let i = 0, l = this.hooks.length; i < l; i++) {
                let hook = this.hooks[i];
                let result = hook(...arguments);
                if (result) {
                    break;
                }
            }
        }
    }


    SyncLoopHook的用法及实现

    当监听函数被触发的时候,如果该监听函数返回true时则这个监听函数会反复执行,如果返回 undefined 则表示退出循环

    SyncLoopHook为同步循环的执行关系,当监听函数被触发的时候,如果该监听函数返回true时则这个监听函数会反复执行,如果返回 undefined 则表示退出循环,用法如下:

    const {
        SyncWaterfallHook
    } = require("tapable");
    
    let queue = new SyncWaterfallHook(['name']);
    
    // 上一个函数的返回值可以传给下一个函数
    queue.tap('1', function (name) {
        console.log(name, 1);
        return 1;
    });
    queue.tap('2', function (data) {
        console.log(data, 2);
        return 2;
    });
    queue.tap('3', function (data) {
        console.log(data, 3);
    });
    
    queue.call('webpack');
    
    // 执行结果:
    /* 
    webpack 1
    1 2
    2 3
    */


    SyncLoopHook的实现:
    // 钩子是同步的
    class SyncLoopHook {
      // args => ["name"]
      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);
        });
      }
    }


    async* 钩子

    对于Async*类型钩子

    • 支持tap、tapPromise、tapAsync注册

    有三种注册/发布的模式,如下:

    异步订阅调用方法
    tapcallAsync
    tapAsynccallAsync
    tapPromisepromise

    异步并行

    AsyncParallelHook的用法及实现

    不关心监听函数的返回值。

    const {
        AsyncParallelHook
    } = require("tapable");
    
    let queue1 = new AsyncParallelHook(['name']);
    console.time('cost');
    queue1.tap('1', function (name) {
        console.log(name, 1);
    });
    queue1.tap('2', function (name) {
        console.log(name, 2);
    });
    queue1.tap('3', function (name) {
        console.log(name, 3);
    });
    queue1.callAsync('webpack', err => {
        console.timeEnd('cost');
    });
    
    // 执行结果
    /* 
    webpack 1
    webpack 2
    webpack 3
    cost: 4.520ms
    */

    usage - tapAsync

    let queue2 = new AsyncParallelHook(['name']);
    console.time('cost1');
    queue2.tapAsync('1', function (name, cb) {
        setTimeout(() => {
            console.log(name, 1);
            cb();
        }, 1000);
    });
    queue2.tapAsync('2', function (name, cb) {
        setTimeout(() => {
            console.log(name, 2);
            cb();
        }, 2000);
    });
    queue2.tapAsync('3', function (name, cb) {
        setTimeout(() => {
            console.log(name, 3);
            cb();
        }, 3000);
    });
    
    queue2.callAsync('webpack', () => {
        console.log('over');
        console.timeEnd('cost1');
    });
    
    // 执行结果
    /* 
    webpack 1
    webpack 2
    webpack 3
    over
    time: 3004.411ms
    */

    usage - promise

    let queue3 = new AsyncParallelHook(['name']);
    console.time('cost3');
    queue3.tapPromise('1', function (name, cb) {
       return new Promise(function (resolve, reject) {
           setTimeout(() => {
               console.log(name, 1);
               resolve();
           }, 1000);
       });
    });
    
    queue3.tapPromise('1', function (name, cb) {
       return new Promise(function (resolve, reject) {
           setTimeout(() => {
               console.log(name, 2);
               resolve();
           }, 2000);
       });
    });
    
    queue3.tapPromise('1', function (name, cb) {
       return new Promise(function (resolve, reject) {
           setTimeout(() => {
               console.log(name, 3);
               resolve();
           }, 3000);
       });
    });
    
    queue3.promise('webpack')
       .then(() => {
           console.log('over');
           console.timeEnd('cost3');
       }, () => {
           console.log('error');
           console.timeEnd('cost3');
       });
    /* 
    webpack 1
    webpack 2
    webpack 3
    over
    cost3: 3007.925ms
    */
    AsyncParallelHook的实现:
    class SyncParralleHook {  constructor() {    this.tasks = [];
      }  tapAsync(name, task) {    this.tasks.push(task);
      }  callAsync(...args) {    // 拿出最终的函数
        let finalCallBack = args.pop();    let index = 0;    // 类似Promise.all
        let done = () => {
          index++;      if (index === this.tasks.length) {
            finalCallBack();
          }
        };    this.tasks.forEach(task => {
          task(...args, done);
        });
      }
    }


    AsyncParallelBailHook

    只要监听函数的返回值不为 null,就会忽略后面的监听函数执行,直接跳跃到callAsync等触发函数绑定的回调函数,然后执行这个被绑定的回调函数。

    usage - tap

    let queue1 = new AsyncParallelBailHook(['name']);console.time('cost');
    queue1.tap('1', function (name) {    console.log(name, 1);
    });
    queue1.tap('2', function (name) {    console.log(name, 2);    return 'wrong'});
    queue1.tap('3', function (name) {    console.log(name, 3);
    });
    queue1.callAsync('webpack', err => {    console.timeEnd('cost');
    });// 执行结果:/* 
    webpack 1
    webpack 2
    cost: 4.975ms
     */

    usage - tapAsync

    let queue2 = new AsyncParallelBailHook(['name']);
    console.time('cost1');
    queue2.tapAsync('1', function (name, cb) {
        setTimeout(() => {
            console.log(name, 1);
            cb();
        }, 1000);
    });
    queue2.tapAsync('2', function (name, cb) {
        setTimeout(() => {
            console.log(name, 2);
            return 'wrong';// 最后的回调就不会调用了
            cb();
        }, 2000);
    });
    queue2.tapAsync('3', function (name, cb) {
        setTimeout(() => {
            console.log(name, 3);
            cb();
        }, 3000);
    });
    
    queue2.callAsync('webpack', () => {
        console.log('over');
        console.timeEnd('cost1');
    });
    
    // 执行结果:
    /* 
    webpack 1
    webpack 2
    webpack 3
    */

    usage - promise

    let queue3 = new AsyncParallelBailHook(['name']);
    console.time('cost3');
    queue3.tapPromise('1', function (name, cb) {
        return new Promise(function (resolve, reject) {
            setTimeout(() => {
                console.log(name, 1);
                resolve();
            }, 1000);
        });
    });
    
    queue3.tapPromise('2', function (name, cb) {
        return new Promise(function (resolve, reject) {
            setTimeout(() => {
                console.log(name, 2);
                reject('wrong');// reject()的参数是一个不为null的参数时,最后的回调就不会再调用了
            }, 2000);
        });
    });
    
    queue3.tapPromise('3', function (name, cb) {
        return new Promise(function (resolve, reject) {
            setTimeout(() => {
                console.log(name, 3);
                resolve();
            }, 3000);
        });
    });
    
    queue3.promise('webpack')
        .then(() => {
            console.log('over');
            console.timeEnd('cost3');
        }, () => {
            console.log('error');
            console.timeEnd('cost3');
        });
    
    // 执行结果:
    /* 
    webpack 1
    webpack 2
    error
    cost3: 2009.970ms
    webpack 3
    */
    AsyncSeriesHook的实现:
    class SyncSeriesHook {
      constructor() {
        this.tasks = [];
      }
      tapAsync(name, task) {
        this.tasks.push(task);
      }
      callAsync(...args) {
        let finalCallback = args.pop();
        let index = 0;
        let next = () => {
          if (this.tasks.length === index) return finalCallback();
          let task = this.tasks[index++];
          task(...args, next);
        };
        next();
      }
    }


    异步串行

    AsyncSeriesWaterfallHook的用法及实现

    不关系callback()的参数

    AsyncSeriesWaterfallHook为异步串行的执行关系,上一个监听函数的中的callback(err, data)的第二个参数,可以作为下一个监听函数的参数

    usage - tap

    const {
        AsyncSeriesHook
    } = require("tapable");
    
    // tap
    let queue1 = new AsyncSeriesHook(['name']);
    console.time('cost1');
    queue1.tap('1', function (name) {
        console.log(1);
        return "Wrong";
    });
    queue1.tap('2', function (name) {
        console.log(2);
    });
    queue1.tap('3', function (name) {
        console.log(3);
    });
    queue1.callAsync('zfpx', err => {
        console.log(err);
        console.timeEnd('cost1');
    });
    // 执行结果
    /* 
    1
    2
    3
    undefined
    cost1: 3.933ms
    */

    usage - tapAsync

    let queue2 = new AsyncSeriesHook(['name']);
    console.time('cost2');
    queue2.tapAsync('1', function (name, cb) {
        setTimeout(() => {
            console.log(name, 1);
            cb();
        }, 1000);
    });
    queue2.tapAsync('2', function (name, cb) {
        setTimeout(() => {
            console.log(name, 2);
            cb();
        }, 2000);
    });
    queue2.tapAsync('3', function (name, cb) {
        setTimeout(() => {
            console.log(name, 3);
            cb();
        }, 3000);
    });
    
    queue2.callAsync('webpack', (err) => {
        console.log(err);
        console.log('over');
        console.timeEnd('cost2');
    }); 
    // 执行结果
    /* 
    webpack 1
    webpack 2
    webpack 3
    undefined
    over
    cost2: 6019.621ms
    */

    usage - promise

    let queue3 = new AsyncSeriesHook(['name']);
    console.time('cost3');
    queue3.tapPromise('1',function(name){
       return new Promise(function(resolve){
           setTimeout(function(){
               console.log(name, 1);
               resolve();
           },1000)
       });
    });
    queue3.tapPromise('2',function(name,callback){
        return new Promise(function(resolve){
            setTimeout(function(){
                console.log(name, 2);
                resolve();
            },2000)
        });
    });
    queue3.tapPromise('3',function(name,callback){
        return new Promise(function(resolve){
            setTimeout(function(){
                console.log(name, 3);
                resolve();
            },3000)
        });
    });
    queue3.promise('webapck').then(err=>{
        console.log(err);
        console.timeEnd('cost3');
    });
    
    // 执行结果
    /* 
    webapck 1
    webapck 2
    webapck 3
    undefined
    cost3: 6021.817ms
    */

    原理

    class AsyncSeriesHook_MY {
        constructor() {
            this.hooks = [];
        }
    
        tapAsync(name, fn) {
            this.hooks.push(fn);
        }
    
        callAsync() {
            var slef = this;
            var args = Array.from(arguments);
            let done = args.pop();
            let idx = 0;
    
            function next(err) {
                // 如果next的参数有值,就直接跳跃到 执行callAsync的回调函数
                if (err) return done(err);
                let fn = slef.hooks[idx++];
                fn ? fn(...args, next) : done();
            }
            next();
        }
    }

    AsyncSeriesBailHook

    callback()的参数不为null,就会直接执行callAsync等触发函数绑定的回调函数

    usage - tap

    const {
        AsyncSeriesBailHook
    } = require("tapable");
    
    // tap
    let queue1 = new AsyncSeriesBailHook(['name']);
    console.time('cost1');
    queue1.tap('1', function (name) {
        console.log(1);
        return "Wrong";
    });
    queue1.tap('2', function (name) {
        console.log(2);
    });
    queue1.tap('3', function (name) {
        console.log(3);
    });
    queue1.callAsync('webpack', err => {
        console.log(err);
        console.timeEnd('cost1');
    });
    
    // 执行结果:
    /* 
    1
    null
    cost1: 3.979ms
    */

    usage - tapAsync

    let queue2 = new AsyncSeriesBailHook(['name']);
    console.time('cost2');
    queue2.tapAsync('1', function (name, callback) {
        setTimeout(function () {
            console.log(name, 1);
            callback();
        }, 1000)
    });
    queue2.tapAsync('2', function (name, callback) {
        setTimeout(function () {
            console.log(name, 2);
            callback('wrong');
        }, 2000)
    });
    queue2.tapAsync('3', function (name, callback) {
        setTimeout(function () {
            console.log(name, 3);
            callback();
        }, 3000)
    });
    queue2.callAsync('webpack', err => {
        console.log(err);
        console.log('over');
        console.timeEnd('cost2');
    });
    // 执行结果
    
    /* 
    webpack 1
    webpack 2
    wrong
    over
    cost2: 3014.616ms
    */

    usage - promise

    let queue3 = new AsyncSeriesBailHook(['name']);
    console.time('cost3');
    queue3.tapPromise('1', function (name) {
        return new Promise(function (resolve, reject) {
            setTimeout(function () {
                console.log(name, 1);
                resolve();
            }, 1000)
        });
    });
    queue3.tapPromise('2', function (name, callback) {
        return new Promise(function (resolve, reject) {
            setTimeout(function () {
                console.log(name, 2);
                reject();
            }, 2000)
        });
    });
    queue3.tapPromise('3', function (name, callback) {
        return new Promise(function (resolve) {
            setTimeout(function () {
                console.log(name, 3);
                resolve();
            }, 3000)
        });
    });
    queue3.promise('webpack').then(err => {
        console.log(err);
        console.log('over');
        console.timeEnd('cost3');
    }, err => {
        console.log(err);
        console.log('error');
        console.timeEnd('cost3');
    });
    // 执行结果:
    /* 
    webpack 1
    webpack 2
    undefined
    error
    cost3: 3017.608ms
    */

    AsyncSeriesWaterfallHook

    上一个监听函数的中的callback(err, data)的第二个参数,可以作为下一个监听函数的参数

    usage - tap

    const {
        AsyncSeriesWaterfallHook
    } = require("tapable");
    
    // tap
    let queue1 = new AsyncSeriesWaterfallHook(['name']);
    console.time('cost1');
    queue1.tap('1', function (name) {
        console.log(name, 1);
        return 'lily'
    });
    queue1.tap('2', function (data) {
        console.log(2, data);
        return 'Tom';
    });
    queue1.tap('3', function (data) {
        console.log(3, data);
    });
    queue1.callAsync('webpack', err => {
        console.log(err);
        console.log('over');
        console.timeEnd('cost1');
    });
    
    // 执行结果:
    /* 
    webpack 1
    2 'lily'
    3 'Tom'
    null
    over
    cost1: 5.525ms
    */

    usage - tapAsync

    let queue2 = new AsyncSeriesWaterfallHook(['name']);
    console.time('cost2');
    queue2.tapAsync('1', function (name, callback) {
        setTimeout(function () {
            console.log('1: ', name);
            callback(null, 2);
        }, 1000)
    });
    queue2.tapAsync('2', function (data, callback) {
        setTimeout(function () {
            console.log('2: ', data);
            callback(null, 3);
        }, 2000)
    });
    queue2.tapAsync('3', function (data, callback) {
        setTimeout(function () {
            console.log('3: ', data);
            callback(null, 3);
        }, 3000)
    });
    queue2.callAsync('webpack', err => {
        console.log(err);
        console.log('over');
        console.timeEnd('cost2');
    });
    // 执行结果:
    /* 
    1:  webpack
    2:  2
    3:  3
    null
    over
    cost2: 6016.889ms
    */

    usage - promise

    let queue3 = new AsyncSeriesWaterfallHook(['name']);
    console.time('cost3');
    queue3.tapPromise('1', function (name) {
        return new Promise(function (resolve, reject) {
            setTimeout(function () {
                console.log('1:', name);
                resolve('1');
            }, 1000)
        });
    });
    queue3.tapPromise('2', function (data, callback) {
        return new Promise(function (resolve) {
            setTimeout(function () {
                console.log('2:', data);
                resolve('2');
            }, 2000)
        });
    });
    queue3.tapPromise('3', function (data, callback) {
        return new Promise(function (resolve) {
            setTimeout(function () {
                console.log('3:', data);
                resolve('over');
            }, 3000)
        });
    });
    queue3.promise('webpack').then(err => {
        console.log(err);
        console.timeEnd('cost3');
    }, err => {
        console.log(err);
        console.timeEnd('cost3');
    });
    // 执行结果:
    /* 
    1: webpack
    2: 1
    3: 2
    over
    cost3: 6016.703ms
    */

    原理

    class AsyncSeriesWaterfallHook_MY {
        constructor() {
            this.hooks = [];
        }
    
        tapAsync(name, fn) {
            this.hooks.push(fn);
        }
    
        callAsync() {
            let self = this;
            var args = Array.from(arguments);
    
            let done = args.pop();
            console.log(args);
            let idx = 0;
            let result = null;
    
            function next(err, data) {
                if (idx >= self.hooks.length) return done();
                if (err) {
                    return done(err);
                }
                let fn = self.hooks[idx++];
                if (idx == 1) {
    
                    fn(...args, next);
                } else {
                    fn(data, next);
                }
            }
            next();
        }
    }



    参考文章:

    webpack插件机制之Tapable https://juejin.cn/post/6844903774645911566

    干货!撸一个webpack插件(内含tapable详解+webpack流程) https://juejin.cn/post/6844903713312604173

    webpack详解 https://juejin.cn/post/6844903573675835400

    webpack4.0源码分析之Tapable https://juejin.cn/post/6844903588112629767

    Webpack 源码(一)—— Tapable 和 事件流 https://segmentfault.com/a/1190000008060440






    转载本站文章《webpack原理(3):Tapable源码分析及钩子函数作用分析》,
    请注明出处:https://www.zhoulujun.cn/html/tools/Bundler/webpackTheory/8754.html