tapable

2019-10-11 tapable

# tapable

#

# 前言

Webpack 是一个现代 JavaScript 应用程序的静态模块打包器,是对前端项目实现自动化和优化必不可少的工具,Webpack 的 loader(加载器)和 plugin(插件)是由 Webpack 开发者和社区开发者共同贡献的,而目前又没有比较系统的开发文档,想写加载器和插件必须要懂 Webpack 的原理,即看懂 Webpack 的源码,tapable 则是 Webpack 依赖的核心库,可以说不懂 tapable 就看不懂 Webpack 源码,所以本篇会对 tapable 提供的类进行解析和模拟。

# tapable 介绍

Webpack 本质上是一种事件流的机制,它的工作流程就是将各个插件串联起来,而实现这一切的核心就是 tapable,Webpack 中最核心的,负责编译的 Compiler 和负责创建 bundlesCompilation 都是 tapable 构造函数的实例。 打开 Webpack 4.0 的源码中一定会看到下面这些以 SyncAsync 开头,以 Hook 结尾的方法,这些都是 tapable 核心库的类,为我们提供不同的事件流执行机制,我们称为 “钩子”。

// 引入 tapable 如下
const {
    SyncHook,
    SyncBailHook,
    SyncWaterfallHook,
    SyncLoopHook,
    AsyncParallelHook,
    AsyncParallelBailHook,
    AsyncSeriesHook,
    AsyncSeriesBailHook,
    AsyncSeriesWaterfallHook
 } = require("tapable");
复制代码

上面的实现事件流机制的 “钩子” 大方向可以分为两个类别,“同步” 和 “异步”,“异步” 又分为两个类别,“并行” 和 “串行”,而 “同步” 的钩子都是串行的。

# Sync 类型的钩子

# 1、SyncHook

SyncHook 为串行同步执行,不关心事件处理函数的返回值,在触发事件之后,会按照事件注册的先后顺序执行所有的事件处理函数。

// SyncHook 钩子的使用
const { SyncHook } = require("tapable");
// 创建实例
let syncHook = new SyncHook(["name", "age"]);
// 注册事件
syncHook.tap("1", (name, age) => console.log("1", name, age));
syncHook.tap("2", (name, age) => console.log("2", name, age));
syncHook.tap("3", (name, age) => console.log("3", name, age));
// 触发事件,让监听函数执行
syncHook.call("panda", 18);
// 1 panda 18
// 2 panda 18
// 3 panda 18
复制代码

tapable 解构的 SyncHook 是一个类,注册事件需先创建实例,创建实例时支持传入一个数组,数组内存储事件触发时传入的参数,实例的 tap 方法用于注册事件,支持传入两个参数,第一个参数为事件名称,在 Webpack 中一般用于存储事件对应的插件名称(名字随意,只是起到注释作用), 第二个参数为事件处理函数,函数参数为执行 call 方法触发事件时所传入的参数的形参。

// 模拟 SyncHook 类
class SyncHook {
    constructor(args) {
        this.args = args;
        this.tasks = [];
    }
    tap(name, task) {
        this.tasks.push(task);
    }
    call(...args) {
        // 也可在参数不足时抛出异常
        if (args.length < this.args.length) throw new Error("参数不足");
        // 传入参数严格对应创建实例传入数组中的规定的参数,执行时多余的参数为 undefined
        args = args.slice(0, this.args.length);
        // 依次执行事件处理函数
        this.tasks.forEach(task => task(...args));
    }
}
复制代码

tasks 数组用于存储事件处理函数,call 方法调用时传入参数超过创建 SyncHook 实例传入的数组长度时,多余参数可处理为 undefined,也可在参数不足时抛出异常,不灵活,后面的例子中就不再这样写了。

# 2、SyncBailHook

SyncBailHook 同样为串行同步执行,如果事件处理函数执行时有一个返回值不为空(即返回值为 undefined),则跳过剩下未执行的事件处理函数(如类的名字,意义在于保险)。

// SyncBailHook 钩子的使用
const { SyncBailHook } = require("tapable");
// 创建实例
let syncBailHook = new SyncBailHook(["name", "age"]);
// 注册事件
syncBailHook.tap("1", (name, age) => console.log("1", name, age));
syncBailHook.tap("2", (name, age) => {
    console.log("2", name, age);
    return "2";
});
syncBailHook.tap("3", (name, age) => console.log("3", name, age));
// 触发事件,让监听函数执行
syncBailHook.call("panda", 18);
// 1 panda 18
// 2 panda 18
复制代码

通过上面的用法可以看出,SyncHookSyncBailHook 在逻辑上只是 call 方法不同,导致事件的执行机制不同,对于后面其他的 “钩子”,也是 call 的区别,接下来实现 SyncBailHook 类。

// 模拟 SyncBailHook 类
class SyncBailHook {
    constructor(args) {
        this.args = args;
        this.tasks = [];
    }
    tap(name, task) {
        this.tasks.push(task);
    }
    call(...args) {
        // 传入参数严格对应创建实例传入数组中的规定的参数,执行时多余的参数为 undefined
        args = args.slice(0, this.args.length);
        // 依次执行事件处理函数,如果返回值不为空,则停止向下执行
        let i = 0, ret;
        do {
            ret = this.tasks[i++](...args);
        } while (!ret);
    }
}
复制代码

在上面代码的 call 方法中,我们设置返回值为 ret,第一次执行后没有返回值则继续循环执行,如果有返回值则立即停止循环,即实现 “保险” 的功能。

# 3、SyncWaterfallHook

SyncWaterfallHook 为串行同步执行,上一个事件处理函数的返回值作为参数传递给下一个事件处理函数,依次类推,正因如此,只有第一个事件处理函数的参数可以通过 call 传递,而 call 的返回值为最后一个事件处理函数的返回值。

// SyncWaterfallHook 钩子的使用
const { SyncWaterfallHook } = require("tapable");
// 创建实例
let syncWaterfallHook = new SyncWaterfallHook(["name", "age"]);
// 注册事件
syncWaterfallHook.tap("1", (name, age) => {
    console.log("1", name, age);
    return "1";
});
syncWaterfallHook.tap("2", data => {
    console.log("2", data);
    return "2";
});
syncWaterfallHook.tap("3", data => {
    console.log("3", data);
    return "3"
});
// 触发事件,让监听函数执行
let ret = syncWaterfallHook.call("panda", 18);
console.log("call", ret);
// 1 panda 18
// 2 1
// 3 2
// call 3
复制代码

SyncWaterfallHook 名称中含有 “瀑布”,通过上面代码可以看出 “瀑布” 形象生动的描绘了事件处理函数执行的特点,与 SyncHookSyncBailHook 的区别就在于事件处理函数返回结果的流动性,接下来看一下 SyncWaterfallHook 类的实现。

// 模拟 SyncWaterfallHook 类
class SyncWaterfallHook {
    constructor(args) {
        this.args = args;
        this.tasks = [];
    }
    tap(name, task) {
        this.tasks.push(task);
    }
    call(...args) {
        // 传入参数严格对应创建实例传入数组中的规定的参数,执行时多余的参数为 undefined
        args = args.slice(0, this.args.length);
        // 依次执行事件处理函数,事件处理函数的返回值作为下一个事件处理函数的参数
        let [first, ...others] = this.tasks;
        return reduce((ret, task) => task(ret), first(...args));
    }
}
复制代码

上面代码中 call 的逻辑是将存储事件处理函数的 tasks 拆成两部分,分别为第一个事件处理函数,和存储其余事件处理函数的数组,使用 reduce 进行归并,将第一个事件处理函数执行后的返回值作为归并的初始值,依次调用其余事件处理函数并传递上一次归并的返回值。

# 4、SyncLoopHook

SyncLoopHook 为串行同步执行,事件处理函数返回 true 表示继续循环,即循环执行当前事件处理函数,返回 undefined 表示结束循环,SyncLoopHookSyncBailHook 的循环不同,SyncBailHook 只决定是否继续向下执行后面的事件处理函数,而 SyncLoopHook 的循环是指循环执行每一个事件处理函数,直到返回 undefined 为止,才会继续向下执行其他事件处理函数,执行机制同理。

// SyncLoopHook 钩子的使用
const { SyncLoopHook } = require("tapable");
// 创建实例
let syncLoopHook = new SyncLoopHook(["name", "age"]);
// 定义辅助变量
let total1 = 0;
let total2 = 0;
// 注册事件
syncLoopHook.tap("1", (name, age) => {
    console.log("1", name, age, total1);
    return total1++ < 2 ? true : undefined;
});
syncLoopHook.tap("2", (name, age) => {
    console.log("2", name, age, total2);
    return total2++ < 2 ? true : undefined;
});
syncLoopHook.tap("3", (name, age) => console.log("3", name, age));
// 触发事件,让监听函数执行
syncLoopHook.call("panda", 18);
// 1 panda 18 0
// 1 panda 18 1
// 1 panda 18 2
// 2 panda 18 0
// 2 panda 18 1
// 2 panda 18 2
// 3 panda 18
复制代码

通过上面的执行结果可以清楚的看到 SyncLoopHook 的执行机制,但有一点需要注意,返回值必须严格是 true 才会触发循环,多次执行当前事件处理函数,必须严格返回 undefined,才会结束循环,去执行后面的事件处理函数,如果事件处理函数的返回值不是 true 也不是 undefined,则会死循环。 在了解 SyncLoopHook 的执行机制以后,我们接下来看看 SyncLoopHookcall 方法是如何实现的。

// 模拟 SyncLoopHook 类
class SyncLoopHook {
    constructor(args) {
        this.args = args;
        this.tasks = [];
    }
    tap(name, task) {
        this.tasks.push(task);
    }
    call(...args) {
        // 传入参数严格对应创建实例传入数组中的规定的参数,执行时多余的参数为 undefined
        args = args.slice(0, this.args.length);
        // 依次执行事件处理函数,如果返回值为 true,则继续执行当前事件处理函数
        // 直到返回 undefined,则继续向下执行其他事件处理函数
        this.tasks.forEach(task => {
            let ret;
            do {
                ret = this.task(...args);
            } while (ret === true || !(ret === undefined));
        });
    }
}
复制代码

在上面代码中可以看到 SyncLoopHookcall 方法的实现更像是 SyncHookSyncBailHookcall 方法的结合版,外层循环整个 tasks 事件处理函数队列,内层通过返回值进行循环,控制每一个事件处理函数的执行次数。 注意:在 Sync 类型 “钩子” 下执行的插件都是顺序执行的,只能使用 tab 注册。


# Async 类型的钩子

Async 类型可以使用 taptapSynctapPromise 注册不同类型的插件 “钩子”,分别通过 callcallAsyncpromise 方法调用,我们下面会针对 AsyncParallelHookAsyncSeriesHookasyncpromise 两种方式分别介绍和模拟。

# 1、AsyncParallelHook

AsyncParallelHook 为异步并行执行,通过 tapAsync 注册的事件,通过 callAsync 触发,通过 tapPromise 注册的事件,通过 promise 触发(返回值可以调用 then 方法)。

# (1) tapAsync/callAsync

callAsync 的最后一个参数为回调函数,在所有事件处理函数执行完毕后执行。

// AsyncParallelHook 钩子:tapAsync/callAsync 的使用
const { AsyncParallelHook } = require("tapable");
// 创建实例
let asyncParallelHook = new AsyncParallelHook(["name", "age"]);
// 注册事件
console.time("time");
asyncParallelHook.tapAsync("1", (name, age, done) => {
    settimeout(() => {
        console.log("1", name, age, new Date());
        done();
    }, 1000);
});
asyncParallelHook.tapAsync("2", (name, age, done) => {
    settimeout(() => {
        console.log("2", name, age, new Date());
        done();
    }, 2000);
});
asyncParallelHook.tapAsync("3", (name, age, done) => {
    settimeout(() => {
        console.log("3", name, age, new Date());
        done();
        console.timeEnd("time");
    }, 3000);
});
// 触发事件,让监听函数执行
asyncParallelHook.callAsync("panda", 18, () => {
    console.log("complete");
});
// 1 panda 18 2018-08-07T10:38:32.675Z
// 2 panda 18 2018-08-07T10:38:33.674Z
// 3 panda 18 2018-08-07T10:38:34.674Z
// complete
// time: 3005.060ms
复制代码

异步并行是指,事件处理函数内三个定时器的异步操作最长时间为 3s,而三个事件处理函数执行完成总共用时接近 3s,所以三个事件处理函数是几乎同时执行的,不需等待。 所有 tabAsync 注册的事件处理函数最后一个参数都为一个回调函数 done,每个事件处理函数在异步代码执行完毕后调用 done 函数,则可以保证 callAsync 会在所有异步函数都执行完毕后执行,接下来看一看 callAsync 是如何实现的。

// 模拟 AsyncParallelHook 类:tapAsync/callAsync
class AsyncParallelHook {
    constructor(args) {
        this.args = args;
        this.tasks = [];
    }
    tabAsync(name, task) {
        this.tasks.push(task);
    }
    callAsync(...args) {
        // 先取出最后传入的回调函数
        let finalCallback = args.pop();
        // 传入参数严格对应创建实例传入数组中的规定的参数,执行时多余的参数为 undefined
        args = args.slice(0, this.args.length);
        // 定义一个 i 变量和 done 函数,每次执行检测 i 值和队列长度,决定是否执行 callAsync 的回调函数
        let i = 0;
        let done = () => {
            if (++i === this.tasks.length) {
                finalCallback();
            }
        };
        // 依次执行事件处理函数
        this.tasks.forEach(task => task(...args, done));
    }
}
复制代码

callAsync 中,将最后一个参数(所有事件处理函数执行完毕后执行的回调)取出,并定义 done 函数,通过比较 i 和存储事件处理函数的数组 taskslength 来确定回调是否执行,循环执行每一个事件处理函数并将 done 作为最后一个参数传入,所以每个事件处理函数内部的异步操作完成时,执行 done 就是为了检测是不是该执行 callAsync 的回调,当所有事件处理函数均执行完毕满足 done 函数内部 ilength 相等的条件时,则调用 callAsync 的回调。

# (2) tapPromise/promise

要使用 tapPromise 注册事件,对事件处理函数有一个要求,必须返回一个 Promise 实例,而 promise 方法也返回一个 Promise 实例,callAsync 的回调函数在 promise 方法中用 then 的方式代替。

// AsyncParallelHook 钩子:tapPromise/promise 的使用
const { AsyncParallelHook } = require("tapable");
// 创建实例
let asyncParallelHook = new AsyncParallelHook(["name", "age"]);
// 注册事件
console.time("time");
asyncParallelHook.tapPromise("1", (name, age) => {
    return new Promise((resolve, reject) => {
        settimeout(() => {
            console.log("1", name, age, new Date());
            resolve("1");
        }, 1000);
    });
});
asyncParallelHook.tapPromise("2", (name, age) => {
    return new Promise((resolve, reject) => {
        settimeout(() => {
            console.log("2", name, age, new Date());
            resolve("2");
        }, 2000);
    });
});
asyncParallelHook.tapPromise("3", (name, age) => {
    return new Promise((resolve, reject) => {
        settimeout(() => {
            console.log("3", name, age, new Date());
            resolve("3");
            console.timeEnd("time");
        }, 3000);
    });
});
// 触发事件,让监听函数执行
asyncParallelHook.promise("panda", 18).then(ret => {
    console.log(ret);
});
// 1 panda 18 2018-08-07T12:17:21.741Z
// 2 panda 18 2018-08-07T12:17:22.736Z
// 3 panda 18 2018-08-07T12:17:23.739Z
// time: 3006.542ms
// [ '1', '2', '3' ]
复制代码

上面每一个 tapPromise 注册事件的事件处理函数都返回一个 Promise 实例,并将返回值传入 resolve 方法,调用 promise 方法触发事件时,如果所有事件处理函数返回的 Promise 实例结果都成功,会将结果存储在数组中,并作为参数传递给 promisethen 方法中成功的回调,如果有一个失败就是将失败的结果返回作为参数传递给失败的回调。

// 模拟 AsyncParallelHook 类 tapPromise/promise
class AsyncParallelHook {
    constructor(args) {
        this.args = args;
        this.tasks = [];
    }
    tapPromise(name, task) {
        this.tasks.push(task);
    }
    promise(...args) {
        // 传入参数严格对应创建实例传入数组中的规定的参数,执行时多余的参数为 undefined
        args = args.slice(0, this.args.length);
        // 将所有事件处理函数转换成 Promise 实例,并发执行所有的 Promise
        return Promise.all(this.tasks.map(task => task(...args)));
    }
}
复制代码

其实根据上面对于 tapPromisepromise 使用的描述就可以猜到,promise 方法的逻辑是通过 Promise.all 来实现的。

# 2、AsyncSeriesHook

AsyncSeriesHook 为异步串行执行,与 AsyncParallelHook 相同,通过 tapAsync 注册的事件,通过 callAsync 触发,通过 tapPromise 注册的事件,通过 promise 触发,可以调用 then 方法。

# (1) tapAsync/callAsync

AsyncParallelHookcallAsync 方法类似,AsyncSeriesHookcallAsync 方法也是通过传入回调函数的方式,在所有事件处理函数执行完毕后执行 callAsync 的回调函数。

// AsyncSeriesHook 钩子:tapAsync/callAsync 的使用
const { AsyncSeriesHook } = require("tapable");
// 创建实例
let asyncSeriesHook = new AsyncSeriesHook(["name", "age"]);
// 注册事件
console.time("time");
asyncSeriesHook.tapAsync("1", (name, age, next) => {
    settimeout(() => {
        console.log("1", name, age, new Date());
        next();
    }, 1000);
});
asyncSeriesHook.tapAsync("2", (name, age, next) => {
    settimeout(() => {
        console.log("2", name, age, new Date());
        next();
    }, 2000);
});
asyncSeriesHook.tapAsync("3", (name, age, next) => {
    settimeout(() => {
        console.log("3", name, age, new Date());
        next();
        console.timeEnd("time");
    }, 3000);
});
// 触发事件,让监听函数执行
asyncSeriesHook.callAsync("panda", 18, () => {
    console.log("complete");
});
// 1 panda 18 2018-08-07T14:40:52.896Z
// 2 panda 18 2018-08-07T14:40:54.901Z
// 3 panda 18 2018-08-07T14:40:57.901Z
// complete
// time: 6008.790ms
复制代码

异步串行是指,事件处理函数内三个定时器的异步执行时间分别为 1s2s3s,而三个事件处理函数执行完总共用时接近 6s,所以三个事件处理函数执行是需要排队的,必须一个一个执行,当前事件处理函数执行完才能执行下一个。 AsyncSeriesHook 类的 tabAsync 方法注册的事件处理函数参数中的 next 可以与 AsyncParallelHook 类中 tabAsync 方法参数的 done 进行类比,同为回调函数,不同点在于 AsyncSeriesHookAsyncParallelHookcallAsync 方法的 “并行” 和 “串行” 的实现方式。

// 模拟 AsyncSeriesHook 类:tapAsync/callAsync
class AsyncSeriesHook {
    constructor(args) {
        this.args = args;
        this.tasks = [];
    }
    tabAsync(name, task) {
        this.tasks.push(task);
    }
    callAsync(...args) {
        // 先取出最后传入的回调函数
        let finalCallback = args.pop();
        // 传入参数严格对应创建实例传入数组中的规定的参数,执行时多余的参数为 undefined
        args = args.slice(0, this.args.length);
        // 定义一个 i 变量和 next 函数,每次取出一个事件处理函数执行,并维护 i 的值
        // 直到所有事件处理函数都执行完,调用 callAsync 的回调
        // 如果事件处理函数中没有调用 next,则无法继续
        let i = 0;
        let next = () => {
            let task = this.tasks[i++];
            task ? task(...args, next) : finalCallback();
        };
        next();
    }
}
复制代码

AsyncParallelHook 是通过循环依次执行了所有的事件处理函数,done 方法只为了检测是否已经满足条件执行 callAsync 的回调,如果中间某个事件处理函数没有调用 done,只是不会调用 callAsync 的回调,但是所有的事件处理函数都执行了。 而 AsyncSeriesHooknext 执行机制更像 ExpressKoa 中的中间件,在注册事件的回调中如果不调用 next,则在触发事件时会在没有调用 next 的事件处理函数的位置 “卡死”,即不会继续执行后面的事件处理函数,只有都调用 next 才能继续,而最后一个事件处理函数中调用 next 决定是否调用 callAsync 的回调。

# (2) tapPromise/promise

AsyncParallelHook 类似,tapPromise 注册事件的事件处理函数需要返回一个 Promise 实例,promise 方法最后也返回一个 Promise 实例。

// AsyncSeriesHook 钩子:tapPromise/promise 的使用
const { AsyncSeriesHook } = require("tapable");
// 创建实例
let asyncSeriesHook = new AsyncSeriesHook(["name", "age"]);
// 注册事件
console.time("time");
asyncSeriesHook.tapPromise("1", (name, age) => {
    return new Promise((resolve, reject) => {
        settimeout(() => {
            console.log("1", name, age, new Date());
            resolve("1");
        }, 1000);
    });
});
asyncSeriesHook.tapPromise("2", (name, age) => {
    return new Promise((resolve, reject) => {
        settimeout(() => {
            console.log("2", name, age, new Date());
            resolve("2");
        }, 2000);
    });
});
asyncParallelHook.tapPromise("3", (name, age) => {
    return new Promise((resolve, reject) => {
        settimeout(() => {
            console.log("3", name, age, new Date());
            resolve("3");
            console.timeEnd("time");
        }, 3000);
    });
});
// 触发事件,让监听函数执行
asyncSeriesHook.promise("panda", 18).then(ret => {
    console.log(ret);
});
// 1 panda 18 2018-08-07T14:45:52.896Z
// 2 panda 18 2018-08-07T14:45:54.901Z
// 3 panda 18 2018-08-07T14:45:57.901Z
// time: 6014.291ms
// [ '1', '2', '3' ]
复制代码

分析上面的执行过程,所有的事件处理函数都返回了 Promise 的实例,如果想实现 “串行”,则需要让每一个返回的 Promise 实例都调用 then,并在 then 中执行下一个事件处理函数,这样就保证了只有上一个事件处理函数执行完后才会执行下一个。

// 模拟 AsyncSeriesHook 类 tapPromise/promise
class AsyncSeriesHook {
    constructor(args) {
        this.args = args;
        this.tasks = [];
    }
    tapPromise(name, task) {
        this.tasks.push(task);
    }
    promise(...args) {
        // 传入参数严格对应创建实例传入数组中的规定的参数,执行时多余的参数为 undefined
        args = args.slice(0, this.args.length);
        // 将每个事件处理函数执行并调用返回 Promise 实例的 then 方法
        // 让下一个事件处理函数在 then 方法成功的回调中执行
        let [first, ...others] = this.tasks;
        return others.reduce((promise, task) => {
            return promise.then(() => task(...args));
        }, first(...args));
    }
}
复制代码

上面代码中的 “串行” 是使用 reduce 归并来实现的,首先将存储所有事件处理函数的数组 tasks 解构成两部分,第一个事件处理函数和存储其他事件处理函数的数组 others,对 others 进行归并,将第一个事件处理函数执行后返回的 Promise 实例作为归并的初始值,这样在归并的过程中上一个值始终是上一个事件处理函数返回的 Promise 实例,可以直接调用 then 方法,并在 then 的回调中执行下一个事件处理函数,直到归并完成,将 reduce 最后返回的 Promise 实例作为 promise 方法的返回值,则实现 promise 方法执行后继续调用 then 来实现后续逻辑。

# 对其他异步钩子补充

在上面 Async 异步类型的 “钩子中”,我们只着重介绍了 “串行” 和 “并行”(AsyncParallelHookAsyncSeriesHook)以及回调和 Promise 的两种注册和触发事件的方式,还有一些其他的具有一定特点的异步 “钩子” 我们并没有进行分析,因为他们的机制与同步对应的 “钩子” 非常的相似。 AsyncParallelBailHookAsyncSeriesBailHook 分别为异步 “并行” 和 “串行” 执行的 “钩子”,返回值不为 undefined,即有返回值,则立即停止向下执行其他事件处理函数,实现逻辑可结合 AsyncParallelHookAsyncSeriesHookSyncBailHookAsyncSeriesWaterfallHook 为异步 “串行” 执行的 “钩子”,上一个事件处理函数的返回值作为参数传递给下一个事件处理函数,实现逻辑可结合 AsyncSeriesHookSyncWaterfallHook

# 总结

tapable 源码中,注册事件的方法 tabtapSynctapPromise 和触发事件的方法 callcallAsyncpromise 都是通过 compile 方法快速编译出来的,我们本文中这些方法的实现只是遵照了 tapable 库这些 “钩子” 的事件处理机制进行了模拟,以方便我们了解 tapable,为学习 Webpack 原理做了一个铺垫,在 Webpack 中,这些 “钩子” 的真正作用就是将通过配置文件读取的插件与插件、加载器与加载器之间进行连接,“并行” 或 “串行” 执行,相信在我们对 tapable 中这些 “钩子” 的事件机制有所了解之后,再重新学习 Webpack 的源码应该会有所头绪。

最后更新: 2019-10-11 10:28:36 ├F10: AM┤