webpack 4.0 Tapable 类中的常用钩子函数源码分析

3,059 阅读7分钟

引言

Tapable 是webpack中的基础类,类似于node中的EventEmitter,都是注册监听,然后收发事件,监听函数执行的过程,自身可以被继承或混入到其它模块中。

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

所以如果你想了解webpack的源码,那么先来了解一下Tapable这个基础类显得尤为必要,那接下来让我带你先来了解一下这个类上的常用的 9个钩子函数吧!

安装

npm i Tapable -d

hooks分类

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

hooks详解

每一个钩子都是一个构造函数,所有的构造函数都接收一个可选的参数 这个参数是一个数组,数组里面可以放一些参数 例如:‘name’,当触发hook事件时 需要传入name 参数,然后监听函数中可以获取name参数。

const hook = new Hook([‘name’])
那怎么注册事件呢?

根据不同的钩子函数使用不同的方法注册事件,常用的注册事件的方法有:tap, tapPromise, tapAsync。

那怎么触发事件呢?

同样的,也是根据不同的钩子函数 使用的触发事件的方法也不相同,常用的触发事件的方法有:call,promise, callAsync。

Sync*类型的hooks

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

1、SyncHook
串行同步执行 不关心返回值
用法
const { SyncHook } = require('tapable');
const mySyncHook = new SyncHook(['name', 'age']);
// 为什么叫tap水龙头 接收两个参数,第一个参数是名称(备注:没有任何意义)  第二个参数是一个函数 接收一个参数  name这个name和上面的name对应 age和上面的age对应
mySyncHook.tap('1', function (name, age) {
    console.log(name, age, 1)
    return 'wrong' // 不关心返回值 这里写返回值对结果没有任何影响
});

mySyncHook.tap('2', function (name, age) {
    console.log(name, age, 2)
});

mySyncHook.tap('3', function (name, age) {
    console.log(name, age, 3)
});

mySyncHook.call('liushiyu', '18');
// 执行的结果
// liushiyu 18 1
// liushiyu 18 2
// liushiyu 18 3
SyncHook源码大致实现
class SyncHook {
    constructor () {
        this.hooks = [];
    }
    tap (name, fn) {
        this.hooks.push(fn)
    }
    call () {
        this.hooks.forEach(hook => hook(...arguments));
    }
}
2、SyncBailHook
串行同步执行 有一个返回值不为null就跳过剩下的逻辑
Bail 是保险的意思 有一个出错就不往下执行了
用法 同SyncHook 的用法
const { SyncBailHook } = require('tapable');
const mySyncBailHook = new SyncBailHook(['name', 'age']);
// 输出结果
// liushiyu 18 1
// return 的值不是null 所以剩下的逻辑就不执行了
SyncBailHook源码大致实现
class SyncBailHook {
    constructor () {
        this.hooks = [];
    }
    tap (name, fn) {
        this.hooks.push(fn)
    }
    call () {
        for(let i=0; i<this.hooks.length; i++) {
            let hook = this.hooks[i];
            let result = hook(...arguments);
            if (result) {
                break;
            }
        }
    }
}
3、SyncWaterfallHook
下一个任务要拿到上一个任务的返回值
用法
const { SyncWaterfallHook } = require('tapable');
const mySyncWaterfallHook = new SyncWaterfallHook(['name']);

mySyncWaterfallHook.tap('1', function (name) {
    console.log(name, '1')
    return '1'
})
mySyncWaterfallHook.tap('2', function (name) {
    console.log(name, '2')
    return '2'
})
mySyncWaterfallHook.tap('3', function (name) {
    console.log(name, '3')
})

mySyncWaterfallHook.call('liu')
// 输出结果
// liu 1
// 1 2
// 2 3
SyncWaterfallHook源码大致实现
class SyncWaterfallHook {
    constructor () {
        this.hooks = [];
    }
    tap (name, fn) {
        this.hooks.push(fn)
    }
    call () {
        let result = null
        for(let i=0; i< this.hooks.length; i++) {
            let hook = this.hooks[i];
            if (!i) {
                result = hook(...arguments)
            } else {
                result = hook(result)
            }
        }
    }
}
4、SyncLoopHook
监听函数返回true表示继续循环,返回undefine表示结束循环
用法
const { SyncLoopHook } = require('tapable');
const mySyncLoopHook = new SyncLoopHook(['name']);

let count = 0;
mySyncLoopHook.tap('1', function (name) {
    console.log(count++);
    if (count < name) {
        return true
    } else {
        return
    }
});

mySyncLoopHook.call('4');
// 输出结果
//0
//1
//2
//3
SyncLoopHook源码大致实现
class SyncLoopHook {
    constructor () {
        this.hook;
    }
    tap (name, fn) {
        this.hook = fn
    }
    call () {
        let result = this.hook(...arguments);
        // do{
        //     result = this.hook(...arguments)
        // } while(result)
        while(result) {
            result = this.hook(...arguments)
        }
    }
}

Async*类型的hooks

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

异步并发执行

1、AsyncParallelHook
异步并发执行  
用法

有三种注册方式

// 第一种注册方式 tap
myAsyncParallelHook.tap('1', function (name) {
    console.log(1, name)
})

myAsyncParallelHook.tap('2', function (name) {
    console.log(2, name)
})

myAsyncParallelHook.tap('3', function (name) {
    console.log(3, name)
})

myAsyncParallelHook.callAsync('liu', function () {
    console.log('over')
});
// 1 'liu'
// 2 'liu'
// 3 'liu'
// over

// 第二种注册方式 tapAsync  凡事有异步 必有回调
console.time('cost')
myAsyncParallelHook.tapAsync('1', function (name, callback) {
    setTimeout(function () {
        console.log(1, name)
        callback()
    }, 1000)
    // callback()
})

myAsyncParallelHook.tapAsync('2', function (name, callback) {
    setTimeout(function () {
        console.log(2, name)
        callback()
    }, 2000)
    // callback()
})

myAsyncParallelHook.tapAsync('3', function (name, callback) {
    setTimeout(function () {
        console.log(3, name)
        callback()
    }, 3000)
})

myAsyncParallelHook.callAsync('liu', () => {
    console.log('over')
    console.timeEnd('cost')
});
并行执行 花费的总时间是时间最长的那个
//1 'liu'
//2 'liu'
//3 'liu'
//over
//cost: 3005.083ms

// 第三种注册方式 tapPromise
console.time('cost')
myAsyncParallelHook.tapPromise('1', function (name) {
    return new Promise(function (resolve, reject) {
        setTimeout(function () {
            console.log(1, name)
            resolve()
        }, 1000)
    })
})

myAsyncParallelHook.tapPromise('2', function (name) {
    return new Promise(function (resolve, reject) {
        setTimeout(function () {
            console.log(2, name)
            resolve()
        }, 2000)
    })
})

myAsyncParallelHook.tapPromise('3', function (name) {
    return new Promise(function (resolve, reject) {
        setTimeout(function () {
            console.log(3, name)
            resolve()
        }, 3000)
    })
})

myAsyncParallelHook.promise('liu').then(function () {
    console.log('ok')
    console.timeEnd('cost')
}, function () {
    console.log('error')
    console.timeEnd('cost')
});

// 1 'liu'
// 2 'liu'
// 3 'liu'
// ok
// cost: 3001.903ms

...

2、AsyncParallelBailHook

有一个失败了 其他的都不用走了
用法
const { AsyncParallelBailHook } = require('tapable');
const myAsyncParallelBailHook = new AsyncParallelBailHook(['name']);

// 第一种注册方式 tap
myAsyncParallelBailHook.tap('1', function (name) {
    console.log(1, name)
    return 'wrong'
})

myAsyncParallelBailHook.tap('2', function (name) {
    console.log(2, name)
})

myAsyncParallelBailHook.tap('3', function (name) {
    console.log(3, name)
})

myAsyncParallelBailHook.callAsync('liu', function () {
    console.log('over')
});
// 1 'liu'
// over

// 第二种注册方式 tapAsync  凡事有异步 必有回调
console.time('cost')
myAsyncParallelBailHook.tapAsync('1', function (name, callback) {
    setTimeout(function () {
        console.log(1, name)
        return 'wrong';// 最后的回调就不会调用了
        callback()
    }, 1000)
    // callback()
})

myAsyncParallelBailHook.tapAsync('2', function (name, callback) {
    setTimeout(function () {
        console.log(2, name)
        callback()
    }, 2000)
    // callback()
})

myAsyncParallelBailHook.tapAsync('3', function (name, callback) {
    setTimeout(function () {
        console.log(3, name)
        callback()
    }, 3000)
})

myAsyncParallelBailHook.callAsync('liu', () => {
    console.log('over')
console.timeEnd('cost')
});

// 1 'liu'
// 2 'liu'
// 3 'liu'

// 第三种注册方式 tapPromise
console.time('cost')
myAsyncParallelBailHook.tapPromise('1', function (name) {
    return new Promise(function (resolve, reject) {
        setTimeout(function () {
            console.log(1, name)
            reject('wrong');// reject()的参数是一个不为null的参数时,最后的回调就不会再调用了
        }, 1000)
    })
})

myAsyncParallelBailHook.tapPromise('2', function (name) {
    return new Promise(function (resolve, reject) {
        setTimeout(function () {
            console.log(2, name)
            resolve()
        }, 2000)
    })
})

myAsyncParallelBailHook.tapPromise('3', function (name) {
    return new Promise(function (resolve, reject) {
        setTimeout(function () {
            console.log(3, name)
            resolve()
        }, 3000)
    })
})

myAsyncParallelBailHook.promise('liu').then(function () {
    console.log('ok')
    console.timeEnd('cost')
}, function () {
    console.log('error')
    console.timeEnd('cost')
});


// 1 'liu'
// error
// cost: 1006.030ms
// 2 'liu'
// 3 'liu'

异步串行执行

1、AsyncSeriesHook
用法
let { AsyncSeriesHook } = require('tapable');
let myAsyncSeriesHook = new AsyncSeriesHook(['name']);
console.time('coast')
myAsyncSeriesHook.tapAsync('1', function (name, cb) {
    setTimeout(function () {
        console.log('1', name)
        cb()
    }, 1000)
});

myAsyncSeriesHook.tapAsync('2', function (name, cb) {
    setTimeout(function () {
        console.log('2', name)
        cb()
    }, 2000)
});

myAsyncSeriesHook.tapAsync('3', function (name, cb) {
    setTimeout(function () {
        console.log('3', name)
        cb()
    }, 3000)
});

myAsyncSeriesHook.callAsync('liu', function () {
    console.log('over')
    console.timeEnd('coast')
})

// 1 liu
// 2 liu
// 3 liu
// over
// coast: 6010.515ms 异步串行执行消耗的时间是所有的总和
AsyncSeriesHook源码大致实现
class AsyncSeriesHook{
    constructor() {
        this.hooks = [];
    }
    tapAsync () {
        this.hooks.push(arguments[arguments.length-1]);
    }
    callAsync () {
        let args = Array.from(arguments); //将传进来的参数转化为数组
        let done = args.pop(); // 取出数组的最后一项 即成功后的回调函数
        let index = 0;
        let that = this;
        function next(err) {
            if (err) return done();
            let fn = that.hooks[index++];
            fn ? fn(...args, next) : done();
        }
        next()
    }
}
2、AsyncSeriesBailHook
用法
// 异步串行执行
let { AsyncSeriesBailHook } = require('tapable');
let myAsyncSeriesBailHook = new AsyncSeriesBailHook(['name']);
console.time('coast')
myAsyncSeriesBailHook.tapAsync('1', function (name, cb) {
    setTimeout(function () {
        console.log('1', name)
        cb('wrong')
    }, 1000)
});

myAsyncSeriesBailHook.tapAsync('2', function (name, cb) {
    setTimeout(function () {
        console.log('2', name)
        cb()
    }, 2000)
});

myAsyncSeriesBailHook.tapAsync('3', function (name, cb) {
    setTimeout(function () {
        console.log('3', name)
        cb()
    }, 3000)
});

myAsyncSeriesBailHook.callAsync('liu', function () {
    console.log('over')
    console.timeEnd('coast')
})
// 1 liu
// over
// coast: 1004.175ms
AsyncSeriesBailHook 源码的大致实现
class AsyncSeriesBailHook{
    constructor() {
        this.hooks = [];
    }
    tapAsync () {
        this.hooks.push(arguments[arguments.length-1]);
    }
    callAsync () {
        let args = Array.from(arguments); //将传进来的参数转化为数组
        let done = args.pop(); // 取出数组的最后一项 即成功后的回调函数
        let index = 0;
        let that = this;
        function next(err) {
            if (err) return done();
            let fn = that.hooks[index++];
            fn ? fn(...args, next) : done();
        }
        next()
    }
}
3、AsyncSeriesWaterfallHook
下一个任务要拿到上一个任务的返回值
用法
// 异步串行执行
let { AsyncSeriesWaterfallHook } = require('tapable');
let myAsyncSeriesWaterfallHook = new AsyncSeriesWaterfallHook(['name']);
console.time('coast')
myAsyncSeriesWaterfallHook.tapAsync('1', function (name, cb) {
    setTimeout(function () {
        console.log('1', name)
        cb(null, 'aa')
    }, 1000)
});

myAsyncSeriesWaterfallHook.tapAsync('2', function (name, cb) {
    setTimeout(function () {
        console.log('2', name)
        cb(null, 'bb')
    }, 2000)
});

myAsyncSeriesWaterfallHook.tapAsync('3', function (name, cb) {
    setTimeout(function () {
        console.log('3', name)
        cb(null, 'cc')
    }, 3000)
});

myAsyncSeriesWaterfallHook.callAsync('liu', function () {
    console.log('over')
    console.timeEnd('coast')
})

// 1 liu
// 2 aa
// 3 bb
// over
// coast: 6011.774ms
AsyncSeriesWaterfallHook 源码的大致实现
class AsyncSeriesWaterfallHook{
    constructor() {
        this.hooks = [];
    }
    tapAsync () {
        this.hooks.push(arguments[arguments.length-1]);
    }
    callAsync () {
        let args = Array.from(arguments); //将传进来的参数转化为数组
        let done = args.pop(); // 取出数组的最后一项 即成功后的回调函数
        let index = 0;
        let that = this;
        function next(err, data) {
            if(index>=that.hooks.length) return done();
            if (err) return done(err);
            let fn = that.hooks[index++];
            if (index == 1) {
                fn(...args, next)
            } else {
                fn(data, next)
            }
        }
        next()
    }
}