webpack 的核心模块:Tapable 的核心 异步方法 模拟实现

642 阅读7分钟

# 前言

咱们书接上回同步方法的实现继续探索下异步方法是如何实现的。

# AsyncParallerHook:异步并行的钩子

AsyncParallerHook.js 文件

// 异步并行的钩子
class AsyncParallerHook {
    constructor(args) {
        this.tasks = [];
    };
    /**
     * 注册监听函数
     * @param {String} name : 名称
     * @param {Function} task : 任务函数
     */
    tapAsync(name, task) {
        this.tasks.push(task);
    };
    /**
     * 执行注册的函数
     * @param  {...any} args
     */
    callAsync(...args) {
        // 取出最终的函数
        let finalCallback = args.pop();
        // 已执行的函数的个数
        let index = 0;
        // 判断所有注册函数是否已经全部执行完成
        let done = () => {
            index++;
            if (index === this.tasks.length) {
                finalCallback(); // z执行最终的回调函数
            }
        }
        // 遍历执行注册函数,并把回调函数转进去
        this.tasks.forEach(task => {
            task(...args, done)
        })
    }
}

module.exports = { AsyncParallerHook };

这里this.tasks.forEach会循环执行每一个函数,并且在函数执行完成之后都会调用一个处理函数done来判断所有注册函数是否全部执行完成。

如果所有注册函数全部执行完成即index === this.tasks.length时,则调用最终的回调函数finalCallback()去处理相应的逻辑。

还是以模拟学习课程为例,代码如下

Lesson.js 文件

const { AsyncParallerHook } = require('./Tapable/AsyncParallerHook');

// 模拟学习的课程
class Lesson {
    constructor() {
        this.hooks = {
            test: new AsyncParallerHook(['name'])
        }
    };
    // 注册钩子
    tapAsync() {
        this.hooks.test.tapAsync('Vue', function(name, cb) {
            setTimeout(_ => {
                console.log('Vue', name)
                cb()
            }, 1000)
        })
        this.hooks.test.tapAsync('Html', function(name, cb) {
            setTimeout(_ => {
                console.log('Html', name)
                cb()
            }, 1000)
        })
    };
    // 启动钩子
    start() {
        this.hooks.test.callAsync('zp', function() {
            console.log('都执行完了')
        });
    };
}

let lesson = new Lesson();

lesson.tapAsync(); // 注册钩子/事件
lesson.start(); // 启动钩子

输出的结果是:Vue zp Html zp 都执行完了

如上:在注册函数里调用AsyncParallerHook的实例方法tapAsync时,里面是用setTimeout来模拟的异步。而这里的cb就是AsyncParallerHook的实例方法callAsync里的done函数,即每次执行完都判断一次所有注册函数是否全部完成。

如果注册全部执行完成时,会调用start启动钩子的函数里的传入的回调函数,也就是对应的AsyncParallerHook的实例方法callAsync里取出的finalCallback函数。

处理这种方式实现,还有另外一种利用promise的实现方式

AsyncParallerHookPromise.js 文件

// 异步并行的钩子
class AsyncParallerHookPromise {
    constructor(args) {
        this.tasks = [];
    };
    /**
     * 注册监听函数
     * @param {String} name : 名称
     * @param {Function} task : 任务函数
     */
    tapPromise(name, task) {
        this.tasks.push(task);
    };
    /**
     * 执行注册的函数
     * @param  {...any} args
     */
    promise(...args) {
        // 依次循环并拿到每次的 promise
        let tasks = this.tasks.map(task => task(...args));
        // 交给 promise.all 执行并返回
        return Promise.all(tasks);
    }
}

module.exports = { AsyncParallerHookPromise };

通过map遍历拿到每一个注册函数的Promise并放在数组里,最后交给Promise.all去执行,最后返回这个Promise.all的结果;

Lesson.js 文件

const { AsyncParallerHookPromise } = require('./Tapable/AsyncParallerHookPromise');

// 模拟学习的课程
class Lesson {
    constructor() {
        this.hooks = {
            test: new AsyncParallerHookPromise(['name'])
        }
    };
    // 注册钩子
    tapAsync() {
        this.hooks.test.tapPromise('Vue', function(name) {
            return new Promise((resolve, reject) => {
                setTimeout(_ => {
                    console.log('Vue', name)
                    resolve()
                }, 1000)
            })
        })
        this.hooks.test.tapPromise('Html', function(name) {
            return new Promise((resolve, reject) => {
                setTimeout(_ => {
                    console.log('Html', name)
                    resolve()
                }, 1000)
            })
        })
    };
    // 启动钩子
    start() {
        this.hooks.test.promise('zp').then(function() {
            console.log('都执行完了')
        });
    };
}

let lesson = new Lesson();

lesson.tapAsync(); // 注册钩子/事件
lesson.start(); // 启动钩子

如上:在注册函数tapAsync里是返回一个new Promise对象,因此才能在AsyncParallerHookPromise实例方法tapPromise里通过map遍历拿到Promise数组。

在启动函数start里调用promise方法通过.then的方式去获得结果,是因为在AsyncParallerHookPromise实例方法promise里最终也是通过Promise.all方式返回了一个Promise对象。

# AsyncSeriesHook:异步串行

AsynSeriseHook.js 文件

// 异步串行的钩子
class AsynSeriseHook {
    constructor(args) {
        this.tasks = [];
    };
    /**
     * 注册监听函数
     * @param {String} name : 名称
     * @param {Function} task : 任务函数
     */
    tapAsync(name, task) {
        this.tasks.push(task);
    };
    /**
     * 执行注册的函数
     * @param  {...any} args
     */
    callAsync(...args) {
        let finalCallback = args.pop();
        let index = 0;
        let next = () => {
            // 这里是递归,注意结束条件防止溢出
            if (index === this.tasks.length) {
                finalCallback();
                return;
            }
            let task = this.tasks[index++];
            task(...args, next)
        }

        next();
    }
}

module.exports = { AsynSeriseHook };

利用递归的形式默认先执行一次next()index = 0,这样就得到了第一次执行的结果。执行完成后index++接着执行下一次,以此类推,直到全部执行完成后在去执行finalCallback函数去处理完成后的逻辑。


const { AsynSeriseHook } = require('./Tapable/AsynSeriseHook');

// 模拟学习的课程
class Lesson {
    constructor() {
        this.hooks = {
            test: new AsynSeriseHook(['name'])
        }
    };
    // 注册钩子
    tapAsync() {
        this.hooks.test.tapAsync('Vue', function(name, cb) {
            setTimeout(_ => {
                console.log('Vue', name)
                cb()
            }, 1000)
        })
        this.hooks.test.tapAsync('Html', function(name, cb) {
            setTimeout(_ => {
                console.log('Html', name)
                cb()
            }, 1000)
        })
    };
    // 启动钩子
    start() {
        this.hooks.test.callAsync('zp', function() {
            console.log('都执行完了')
        });
    };
}

let lesson = new Lesson();

lesson.tapAsync(); // 注册钩子/事件
lesson.start(); // 启动钩子

如上:通过setTimeout模拟了异步,并每次执行完成后都执行了回调函数cb。这里的cb就是AsynSeriseHook实例方法callAsync里的next函数,于是就完成了串行。

待所有注册函数执行完成,就会调用callAsync里的回调函数去处理完成逻辑,而这里的回调函数就是AsynSeriseHook实例方法callAsync里的finalCallback函数。

如你所想,还有另一种 Promise 的实现方式

AsyncSeriesHookPromise.js 文件

// 异步串行的钩子
class AsyncSeriesHookPromise {
    constructor(args) {
        this.tasks = [];
    };
    /**
     * 注册监听函数
     * @param {String} name : 名称
     * @param {Function} task : 任务函数
     */
    tapPromise(name, task) {
        this.tasks.push(task);
    };
    /**
     * 执行注册的函数
     * @param  {...any} args
     */
    promise(...args) {
        let [first, ...others] = this.tasks;
        return others.reduce((pre, task) => {
            return pre.then(res => task(...args));
        }, first(...args))
    }
}

module.exports = { AsyncSeriesHookPromise };

首先会取出第一个注册函数先执行,并通过reduce循环遍历剩下的所有注册函数。遍历时通过.then的方式拿到前一次的执行结果,并在拿到结果后接着执行下一个,以此类推形成串行。

Lesson.js 文件

const { AsyncSeriesHookPromise } = require('./Tapable/AsyncSeriesHookPromise');

// 模拟学习的课程
class Lesson {
    constructor() {
        this.hooks = {
            test: new AsyncSeriesHookPromise(['name'])
        }
    };
    // 注册钩子
    tapAsync() {
        this.hooks.test.tapPromise('Vue', function(name) {
            return new Promise((resolve, reject) => {
                setTimeout(_ => {
                    console.log('Vue', name)
                    resolve()
                }, 1000)
            })
        })
        this.hooks.test.tapPromise('Html', function(name) {
            return new Promise((resolve, reject) => {
                setTimeout(_ => {
                    console.log('Html', name)
                    resolve()
                }, 1000)
            })
        })
    };
    // 启动钩子
    start() {
        this.hooks.test.promise('zp').then(function() {
            console.log('都执行完了')
        });
    };
}

let lesson = new Lesson();

lesson.tapAsync(); // 注册钩子/事件
lesson.start(); // 启动钩子

如上:在注册钩子tapAsync里注册函数时,是返回了一个new Promise对象,所以可在对应的AsyncSeriesHookPromise实例方法tapPromise里可通过.then的方式拿到上一次执行的结果,在拿到结果后紧接着执行下一次。

# AsynSeriseHookWaterfall:异步串行的瀑布的钩子

AsynSeriseHookWaterfall.js 文件

// 异步串行的瀑布的钩子
class AsynSeriseHookWaterfall {
    constructor(args) {
        this.tasks = [];
    };
    /**
     * 注册监听函数
     * @param {String} name : 名称
     * @param {Function} task : 任务函数
     */
    tapAsync(name, task) {
        this.tasks.push(task);
    };
    /**
     * 执行注册的函数
     * @param  {...any} args
     */
    callAsync(...args) {
        let finalCallback = args.pop();
        let index = 0;

        let next = (err, data) => {
            let task = this.tasks[index];
            // 这里是递归,注意结束条件防止溢出
            if (!task || err) return finalCallback();
            if (index == 0) {
                task(...args, next)
            } else {
                task(data, next)
            }
            index++;
        }

        next();
    }
}

module.exports = { AsynSeriseHookWaterfall };

在是实现next函数的时候接受两个参数,分别是:

err :上次执行的结果是否发生错误,非null即可视为错误。
data:需要传入下一次的数据。

默认先执行一次,既然是递归就要判断结束条件防止溢出。当拿不到当前注册函数时即index大于了tasks.length时(!task == true)或上个一注册函数发生错误时(err == true),都要终端当前的递归并执行最终的函数finalCallback去处理逻辑。

这里需要注意的是当index == 0时即第一次执行时,拿到的参数是args,当index大于0即不是第一次执行时,参数是上一次注册函数传递过来的即next函数的data参数。

Lesson.js 文件

const { AsynSeriseHookWaterfall } = require('./Tapable/AsynSeriseHookWaterfall');

// 模拟学习的课程
class Lesson {
    constructor() {
        this.hooks = {
            test: new AsynSeriseHookWaterfall(['name'])
        }
    };
    // 注册钩子
    tapAsync() {
        this.hooks.test.tapAsync('Vue', function(name, cb) {
            setTimeout(_ => {
                console.log('Vue', name)
                cb(null, 'Vue 可以了')
            }, 1000);
        })
        this.hooks.test.tapAsync('Html', function(name, cb) {
            setTimeout(_ => {
                console.log('Html', name)
                cb()
            }, 1000);
        })
    };
    // 启动钩子
    start() {
        this.hooks.test.callAsync('zp', function() {
            console.log('都执行完了')
        });
    };
}

let lesson = new Lesson();

lesson.tapAsync(); // 注册钩子/事件
lesson.start(); // 启动钩子

如上:在第一个注册函数里cb传了nullVue 可以了时,表明此次注册函数执行成功,可以执行下一次,并把Vue 可以了传给了下一次的注册函数。也就是AsynSeriseHookWaterfall实例方法callAsync里的next函数的err==null,data == 'Vue 可以了',所以输出结果是:Vue zp Html Vue 可以了都执行完了

this.hooks.test.tapAsync('Vue', function(name, cb) {
    setTimeout(_ => {
        console.log('Vue', name)
        cb('哇哦,出错了', 'Vue 可以了')
    }, 1000);
})

这时cb传的第一个参数不是null了,即AsynSeriseHookWaterfall实例方法callAsync里的next函数的err != null,所以语句(!task || err)true了,这时要终止执行递归函数,并执行finalCallback函数来处理所需逻辑。

如你所想,还有另一种Promise的实现方式

AsynSeriseHookWaterfallPromise.js 文件

// 异步串行并行的钩子
class AsynSeriseHookWaterfallPromise {
    constructor(args) {
        this.tasks = [];
    };
    /**
     * 注册监听函数
     * @param {String} name : 名称
     * @param {Function} task : 任务函数
     */
    tapPromise(name, task) {
        this.tasks.push(task);
    };
    /**
     * 执行注册的函数
     * @param  {...any} args
     */
    promise(...args) {
        let [first, ...others] = this.tasks;
        return others.reduce((pre, task, index) => {
            return pre.then(res => task(res));
        }, first(...args))
    }
}

module.exports = { AsynSeriseHookWaterfallPromise };