webpack中tapable原理详解,一起学习任务流程管理

作者 : 开心源码 本文共5237个字,预计阅读时间需要14分钟 发布时间: 2022-05-13 共186人阅读

学习webpack源码时,总是绕不开tapable,越看越觉得它晦涩难懂,但只需了解了它的功能,学习就会容易很多。

简单来说,有一系列的同步、异步任务,我希望它们可以以多种流程执行,比方:

  • 一个执行完再执行下一个,即串行执行
  • 一块执行,即并行执行
  • 串行执行过程中,可以中断执行,即有熔断机制
  • 等等

tapable库,就帮我们实现了多种任务的执行流程,它们可以根据以下特点分类:

  • 同步sync、异步async**:task能否包含异步代码
  • 串行series、并发parallel**:前后task能否有执行顺序
  • 能否使用promise
  • 熔断bail**:能否有熔断机制
  • waterfall:前后task能否有数据依赖

举个例子,假如我们想要多个同步的任务 串行执行,只要要三个步骤:初始化hook、增加任务、触发任务执行

// 引入 同步 的hookconst { SyncBailHook } =  require("tapable");// 初始化const tasks = new SyncBailHook(['tasks'])// 绑定一个任务tasks.tap('task1', () => {    console.log('task1', name);})// 再绑定一个任务tasks.tap('task2', () => {    console.log('task2', name);})// 调用call,我们的两个任务就会串行执行了,tasks.call('done')

是不是很简单,下面我们学习下tapable实现了哪些任务执行流程,并且是如何实现的:

一、同步事件流

如上例子所示,每一种hook都会有两个方法,用于增加任务触发任务执行。在同步的hook中,分别对应tapcall方法。

1. 并行

所有任务一起执行

class SyncHook {    constructor() {        // 用于保存增加的任务        this.tasks = []    }    tap(name, task) {        // 注册事件        this.tasks.push(task)    }    call(...args) {        // 把注册的事件依次调用,无特殊解决        this.tasks.forEach(task => task(...args))    }}

2. 串行可熔断

假如其中一个task有返回值(不为undefined),就会中断tasks的调用

class SyncBailHook {    constructor() {        // 用于保存增加的任务        this.tasks = []    }    tap(name, task) {        this.tasks.push(task)    }    call(...args) {        for (let i = 0; i < this.tasks.length; i++) {            const result = this.tasks[i](...args)            // 有返回值的话,就会中断调用            if (result !== undefined) {                break            }        }    }}

3. 串行瀑布流

task的计算结果会作为下一个task的参数,以此类推

class SyncWaterfallHook {    constructor() {        this.tasks = []    }    tap(name, task) {        this.tasks.push(task)    }    call(...args) {        const [first, ...others] = this.tasks        const result = first(...args)        // 上一个task的返回值会作为下一个task的函数参数        others.reduce((result, task) => {            return task(result)        }, result)    }}

4. 串行可循环

假如task有返回值(返回值不为undefined),就会循环执行当前task,直到返回值为undefined才会执行下一个task

class SyncLoopHook {    constructor() {        this.tasks = []    }    tap(name, task) {        this.tasks.push(task)    }    call(...args) {        // 当前执行task的index        let currentTaskIdx = 0        while (currentTaskIdx < this.tasks.length) {            let task = this.tasks[currentTaskIdx]            const result = task(...args)            // 只有返回为undefined的时候才会执行下一个task,否则一直执行当前task            if (result === undefined) {                currentTaskIdx++            }        }    }}

二、异步事件流

异步事件流中,绑定和触发的方法都会有两种实现:

  • 使用promisetapPromise绑定、promise触发
  • promisetapAsync绑定、callAsync触发

注意事项:

既然我们要控制异步tasks的执行流程,那我们必需要知道它们执行完的时机:

  • 使用promisehook,任务中resolve的调用就代表异步执行完毕了;

    // 使用promise方法的例子// 初始化异步并行的hookconst asyncHook = new AsyncParallelHook('async')// 增加task// tapPromise需要返回一个promiseasyncHook.tapPromise('render1', (name) => {    return new Promise((resolve, reject) => {        setTimeout(() => {            console.log('render1', name);            resolve()        }, 1000);    })})// 再增加一个task// tapPromise需要返回一个promiseasyncHook.tapPromise('render2', (name) => {    return new Promise((resolve, reject) => {        setTimeout(() => {            console.log('render2', name);            resolve()        }, 1000);    })})// 传入的两个异步任务即可以串行执行了,并在执行完毕后打印doneasyncHook.promise().then( () => {    console.log('done');})
  • 但在使用非promisehook时,异步任务执行完毕的时机我们就无从获取了。所以我们规定传入的 task的最后一个参数参数为一个函数,并且在异步任务执行完毕后执行它,这样我们能获取执行完毕的时机,如下例所示:

    const asyncHook = new AsyncParallelHook('async')// 增加taskasyncHook.tapAsync('example', (data, cb) => {    setTimeout(() => {        console.log('example', name);        // 在异步操作完成时,调用回调函数,表示异步任务完成        cb()    }, 1000);})// 增加taskasyncHook.tapAsync('example1', (data, cb) => {    setTimeout(() => {        console.log('example1', name);        // 在异步操作完成时,调用回调函数,表示异步任务完成        cb()    }, 1000);})// 传入的两个异步任务即可以串行执行了,并在执行完毕后打印doneasyncHook.callAsync('done', () => {    console.log('done')})

1. 并行执行

task一起执行,所有异步事件执行完成后,执行最后的回调。相似promise.all

NOTE: callAsync中计数器的使用,相似于promise.all的实现原理

class AsyncParallelHook {    constructor() {        this.tasks = []    }    tapAsync(name, task) {        this.tasks.push(task)    }    callAsync(...args) {        // 最后一个参数为,流程结束的回调        const finalCB = args.pop()        let index = 0        // 这就是每个task执行完成时调用的回调函数        const CB = () => {            ++index            // 当这个回调函数调用的次数等于tasks的个数时,说明任务都执行完了            if (index === this.tasks.length) {                // 调用流程结束的回调函数                finalCB()            }        }        this.tasks.forEach(task => task(...args, CB))    }    // task是一个promise生成器    tapPromise(name, task) {        this.tasks.push(task)    }    // 使用promise.all实现    promise(...args) {        const tasks = this.tasks.map(task => task(...args))        return Promise.all(tasks)    }}

2. 异步串行执行

所有tasks串行执行,一个tasks执行完了在执行下一个

NOTE:callAsync的实现与使用,相似于generate执行器coasync await的原理

NOTE:promise的实现与使用,就是面试中常见的 异步任务调度题 的正解。比方,实现每隔一秒打印1次,打印5次。

class AsyncSeriesHook {    constructor() {        this.tasks = []    }    tapAsync(name, task) {        this.tasks.push(task)    }    callAsync(...args) {        const finalCB = args.pop()        let index = 0        // 这就是每个task异步执行完毕之后调用的回调函数        const next = () => {            let task = this.tasks[index++]            if (task) {                // task执行完毕之后,会调用next,继续执行下一个task,形成递归,直到任务一律执行完                task(...args, next)            } else {                // 任务完毕之后,调用流程结束的回调函数                finalCB()            }        }        next()    }    tapPromise(name, task) {        this.tasks.push(task)    }    promise(...args) {        let [first, ...others] = this.tasks        return others.reduce((p, n) =>{            // then函数中返回另一个promise,可以实现promise的串行执行            return p.then(() => n(...args))        },first(...args))    }}

3. 串行瀑布流

异步task串行执行,task的计算结果会作为下一个task的参数,以此类推。task执行结果通过cb回调函数向下传递。

class AsyncWaterfallHook {    constructor() {        this.tasks = []    }    tapAsync(name, task) {        this.tasks.push(task)    }    callAsync(...args) {        const [first] = this.tasks        const finalCB = args.pop()        let index = 1        // 这就是每个task异步执行完毕之后调用的回调函数,其中ret为上一个task的执行结果        const next = (error, ret) => {            if(error !== undefined) {                return            }            let task = this.tasks[index++]            if (task) {                // task执行完毕之后,会调用next,继续执行下一个task,形成递归,直到任务一律执行完                task(ret, next)            } else {                // 任务完毕之后,调用流程结束的回调函数                finalCB(ret)            }        }        first(...args, next)    }    tapPromise(name, task) {        this.tasks.push(task)    }    promise(...args) {        let [first, ...others] = this.tasks        return others.reduce((p, n) =>{            // then函数中返回另一个promise,可以实现promise的串行执行            return p.then(() => n(...args))        }, first(...args))    }}

总结

学了tapable的少量hook,你能扩展到很多东西:

  • promise.all
  • co模块
  • async await
  • 面试中的经典手写代码题:任务调度系列
  • 设计模式之监听者模式
  • 设计模式之发布订阅者模式

你都可以去实现,用于巩固和拓展相关知识。

我们在学习tapable时,重点不在于这个库的细节和使用,而在于多个任务有可能的执行流程以及流程的实现原理,它们是众多实际问题的笼统模型,掌握了它们,你即可以在实际开发中和面试中举一反三,举重若轻。

有哪些流程管理方面的面试题呢?写到评论区大家一起学习下!!!

说明
1. 本站所有资源来源于用户上传和网络,如有侵权请邮件联系站长!
2. 分享目的仅供大家学习和交流,您必须在下载后24小时内删除!
3. 不得使用于非法商业用途,不得违反国家法律。否则后果自负!
4. 本站提供的源码、模板、插件等等其他资源,都不包含技术服务请大家谅解!
5. 如有链接无法下载、失效或广告,请联系管理员处理!
6. 本站资源售价只是摆设,本站源码仅提供给会员学习使用!
7. 如遇到加密压缩包,请使用360解压,如遇到无法解压的请联系管理员
开心源码网 » webpack中tapable原理详解,一起学习任务流程管理

发表回复