webpack怎么能只是会用呢,核心中的核心tapable了解下?

3,041

前言

为什么我们要学tapable,因为....webpack源码里面都是用的tapable来实现钩子挂载的,作为一个有点追求的code,webpack怎么能只满足于用呢?当然是要去看源码,写loader,plugin啦.在这之前,要是不清楚tapable的用法,源码那是更不用看了,看不懂.....所以,今天来讲一下tapable吧

1. tapable

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

tapable创建实例时传递的参数对于程序运行并没有任何作用,只是给源码阅读者提供帮助

同样的,在使用tap*注册监听时,传递的第一个参数,也只是一个标识,并不会在程序运行中产生任何影响。而第二个参数则是回调函数

2.tapable的用法

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

3. Sync*类型的钩子

  • 注册在该钩子下面的插件的执行顺序都是顺序执行
  • 只能使用tap注册,不能使用tapPromise和tapAsync注册

3.1 SyncHook

串行同步执行,不关心返回值 在SyncHook的实例上注册了tap之后,只要实例调用了call方法,那么这些tap的回掉函数一定会顺序执行一遍

let queue = new SyncHook(['没任何作用的参数']);

queue.tap(1,(name,age)=>{
    console.log(name,age)
})
queue.tap(2,(name,age)=>{
    console.log(name,age)
})
queue.tap(3,(name,age)=>{
    console.log(name,age)
})
queue.call('bearbao',8)

// 输出结果
// 'bearbao' 8
// 'bearbao' 8
// 'bearbao' 8

3.1.1 SyncHook实现

class SyncHook {
    constructor(){
        this.listeners = [];
    }
    tap(formal,listener){
        this.listeners.push(listener)
    }
    call(...args){
        this.listeners.forEach(l=>l(...args))
    }
}

3.2 SyncBailHook

串行同步执行,有一个返回值不为null则跳过剩下的逻辑


let queue = new SyncBailHook(['name'])

queue.tap(1,name=>{
  console.log(name)  
})
queue.tap(1,name=>{
  console.log(name) 
  return '1'
})
queue.tap(1,name=>{
  console.log(name)  
})

queue.call('bearbao')
// 输出结果,只执行前面两个回调,第三个不执行
// bearbao
// bearbao

实现

class SyncBailHook {
    constructor(){
        this.listeners = [];
    }
    tap(formal,listener){
        this.listeners.push(listener)
    }
    call(...args){
        for(let i=0;i<this.listeners.length;i++){
            if(this.listeners[i]()) break;
        }
    }
}

3.3 SyncWaterHook

串行同步执行,第一个注册的回调函数会接收call传进来的所有参数,之后的每个回调函数只接收到一个参数,就是上一个回调函数的返回值.

let queue = new SyncWaterHook(['name','age']);

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

queue.call('bearbao', 3)

// 输出结果
// bearbao 3
// 1
// 2

SyncWaterHook 实现. SyncWaterHook这个方法很像redux中的compose方法,都是将一个函数的返回值作为参数传递给下一个函数.

对下面实现的call方法如果有疑惑,看不大懂的同学可以移步我之前对于compose函数的解读,里面有详细的介绍,这里就不多加赘述了

Redux进阶compose方法的实现与解析

class SyncWaterHook{
    constructor(){
        this.listeners = [];
    }
    tap(formal,listener){
        this.listener.unshift(listener);
    }
    call(...args){
        this.listeners.reduce((a,b)=>(...args)=>a(b(...args)))(...args)
    }
}

3.4 SyncLoopHook

串行同步执行, 监听函数返回true表示继续循环,返回undefined表示循环结束

let queue = new SyncLoopHook;
let index = 0;
queue.tap(1,_=>{
    index++
    if(index<3){
        console.log(index);
        return true
    }
})
queue.call();

// 输出结果
// 1
// 2

SyncLoopHook实现

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

4. Async*类型的钩子

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

4.1 AsyncParallel

异步并行执行

4.1.1 AsyncParallelHook

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

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

异步订阅 调用方法
tap callAsync
tapAsync callAsync
tapPromise promise
  • 通过tap来使用

触发函数的参数,出了最后一个参数是异步监听回调函数执行完成之后的回调,其他的参数都是传递给回调函数的参数

let queue = new AsyncParallelHook(['name']);
console.time('cost');
queue.tap('1',function(name){
    console.log(name,1);
});
queue.tap('2',function(name){
    console.log(name,2);
});
queue.tap('3',function(name){
    console.log(name,3);
});
queue.callAsync('bearbao',err=>{
    console.log(err);
    console.timeEnd('cost');
});

// 执行结果
/* 
 bearbao 1
 bearbao 2
 bearbao 3
cost: 4.720ms
*/

实现

class AsyncParallelHook {
    constructor(){
        this.listeners = [];
    }
    tap(name,listener){
        this.listeners.push(listener);
    }
    callAsync(){
        this.listeners.forEach(listener=>listener(...arguments));
        Array.from(arguments).pop()();
    }
}

  • 通过tapAsync来注册

注意,这里有个特殊的地方,如何确认某个回调执行完了呢?,每个监听回调的最后一个参数是一个回调函数,当执行callback之后,会认为当前函数执行完毕

let queue = new AsyncParallelHook(['name']);
console.time('cost');
queue.tapAsync('1',function(name,callback){
    setTimeout(function(){
        console.log(name, 1);
        callback();
    },1000)
});
queue.tapAsync('2',function(name,callback){
    setTimeout(function(){
        console.log(name, 2);
        callback();
    },2000)
});
queue.tapAsync('3',function(name,callback){
    setTimeout(function(){
        console.log(name, 3);
        callback();
    },3000)
});
queue.callAsync('bearbao',err=>{
    console.log(err);
    console.timeEnd('cost');
});

// 输出结果
/*
bearbao 1
bearbao 2
bearbao 3
cost: 3000.448974609375ms
*/

实现

class AsyncParallelHook {
    constructor(){
        this.listeners = [];
    }
    tapAsync(name,listener){
        this.listeners.push(listener);
    }
    callAsync(...arg){
        let callback = arg.pop();
        let i = 0;
        let done = ()=>{
            if(++i==this.listeners.length){
                callback()
            }
        }
        this.listeners.forEach(listener=>listener(...arg,done));
        
    }
}

  • 使用tapPromise

使用tapPromise注册监听时,每个回调函数的返回值必须是一个Promise的实例

let queue = new AsyncParallelHook(['name']);
console.time('cost');
queue.tapPromise('1',function(name){
    return new Promise(function(resolve,reject){
        setTimeout(function(){
            console.log(1);
            resolve();
        },1000)
    });

});
queue.tapPromise('2',function(name){
    return new Promise(function(resolve,reject){
        setTimeout(function(){
            console.log(2);
            resolve();
        },2000)
    });
});
queue.tapPromise('3',function(name){
    return new Promise(function(resolve,reject){
        setTimeout(function(){
            console.log(3);
            resolve();
        },3000)
    });
});
queue.promise('bearbao').then(()=>{
    console.timeEnd('cost');
})

// 执行记过
/*
 1
 2
 3
cost: 3000.448974609375ms
*/

实现

class AsyncParallelHook {
    constructor(){
        this.listeners = [];
    }
    tapPromise(name,listener){
        this.listeners.push(listener);
    }
    promise(...arg){
        let i = 0;
        return Promise.all(this.listeners.map(l=>l(arg)))
    }
}

5. 好困好困

一不小心又到1点了,为了能够获得长寿成就,今天就先写到这里吧,后续几个方法,过两天再更新上来

结语

如果觉得还可以,能在诸君的编码之路上带来一点帮助,请点赞鼓励一下,谢谢!