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

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

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

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

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

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

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

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

    const asyncHook = new AsyncParallelHook('async')
    // 添加task
    asyncHook.tapAsync('example', (data, cb) => {
        setTimeout(() => {
            console.log('example', name);
            // 在异步操作完成时,调用回调函数,表示异步任务完成
            cb()
        }, 1000);
    })
    // 添加task
    asyncHook.tapAsync('example1', (data, cb) => {
        setTimeout(() => {
            console.log('example1', name);
            // 在异步操作完成时,调用回调函数,表示异步任务完成
            cb()
        }, 1000);
    })
    // 传入的两个异步任务就可以串行执行了,并在执行完毕后打印done
    asyncHook.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时,重点不在于这个库的细节和使用,而在于多个任务有可能的执行流程以及流程的实现原理,它们是众多实际问题的抽象模型,掌握了它们,你就可以在实际开发中和面试中举一反三,举重若轻。

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,324评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,303评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,192评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,555评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,569评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,566评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,927评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,583评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,827评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,590评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,669评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,365评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,941评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,928评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,159评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,880评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,399评论 2 342

推荐阅读更多精彩内容