Webpack中的Tapable运行机制

122 阅读7分钟

0.前置知识

webpack中插件定义

class myPlugin {
    constructor(options) {
        // 一般在构造函数中完成参数的初始化赋值操作
    }
    apply(compiler) {
        compiler.plugin('done', (compileration) => {
            console.log()
        })
    }
}

module.exports = {
    plugins: [
       new myPlugin(options) 
    ]
}

注意点:
1.为什么注册自定义的plugin除了基础的constructor之外还要定义apply函数?是因为webpack的构建流程在初始化阶段会将config文件做三件事:
(1)将配置文件整体赋值给compiler对象的options属性中;
(2)配置options中的resolver对象,即配置compiler对象的环境变量,方便resolver查找文件地址;
(3)执行options中的plugins数组,执行每一个数组元素的apply方法,挂载对应钩子。所以如果不定义apply方法,那么就没有办法实现插件的挂载监听。

1.Tapable介绍

Webpack 的 Tapable 事件流机制保证了插件的有序性,使得整个系统扩展性良好。
tapable是webpack插件的核心机制,主要原理是利用“发布-订阅”模式上,配合特殊逻辑叠加,控制打包过程中具体阶段调用什么plugin库,支撑webpack各种复杂的事件-处理需求。所以,tapable的核心功能就是对一系列注册事件的执行流控制。

image.png

2.tapable的使用

tap注册,call调用

const { SyncHook } = require('tapable');
// 1.初始化hook实例:这里的创建一个hook实例就是为了给后面所有hook需要用到的变量赋默认值。
const synchook = new SyncHook();
synchook.tap('logan', () => {})
synchook.call();

3.tapable库提供的钩子类型

“钩子” 大方向可以分为两个类别,“同步” 和 “异步”,“异步” 又分为两个类别,“并行” 和 “串行”,而 “同步” 的钩子都是串行的。

3.1 常见的9中钩子

每一种hook都会有tap和call两种方法,分别用于添加任务和触发任务执行。但是对于不同类型的钩子,执行任务的机制是不一样的
(1) 同步钩子执行机制 同步钩子都会带有sync前缀,在此基础上分为四种执行机制的同步钩子:“同步” 的钩子都是串行的 **全部并行

4.tapable各个类型钩子的实现

4.1 基础型钩子原理
就是一个典型的发布订阅模型

class hook {
    constructor() {
        this.stack = {};
    }
    tap(name, callback) { // 注册
        if (!this.stack[name]) {
            this.stack[name] = [];
        }
        this.stack[name].push(callback);
    }
    call(name) { // 发布
        if (this.stack[name]) {
            this.stack[name].forEach((fn_item) => {
                fn_item();
            });
        }
    }
}

4.2 熔断型钩子原理

注册方法是相同的,但是call触发方法不同
callBail() {
  for (let fn_item of taps) {
      let result = undefined;
      result = fn_item();
      if (result !== undefined) { // 如果存在返回值,则中断
          return;
      }
  }
}

4.3 流水型钩子
流水型钩子的订阅者执行的时候,依赖上一个订阅者的返回值, 如果上一个订阅者没有返回值的话,那它会一直往上寻找,直到找到了那个未返回undefined的返回值,上述代码通过参数来确认构造一个求和函数或者求差函数。

注册方法是相同的,但是call触发方法不同
callBail() {
  let res = ''
  for (let fn_item of taps) {
      res = fn_item(res);
  }
}

4.4 循环型钩子

一、同步事件流

SyncHook

--SyncHook**:将按照钩子函数的注册顺序依次同步执行

let { SyncHook } = require('tapable');
//触发此事件需传入name参数,然后监听函数可以获取name参数
let hooks = new SyncHook(["name", "age"]);
//注册监听
hooks.tap("a", function (name, age) {
  console.log(name, age,'a');
});
hooks.tap("b", function (name, age) {
  console.log(name, age,'b');
  // 返回值无效
  return 'error'
});
hooks.tap("c", function (name, age) {
  console.log(name, age,'c');
});
// 触发事件
hooks.call("测试"); 

image.png 这里借用知乎大佬的图:www.zhihu.com/question/47…

SyncBailHook

熔断型钩子--SyncBailHook:如果前一个任务的返回值是undefined,下一个可以继续执行,否则下面任务不再执行 image.png

SyncWaterfallHook

流水型钩子--SyncWaterfallHook:前面一个任务的返回值作为下面一个任务的输入值。 需要额外注意的是当存在多个参数时,通过 SyncWaterfallHook 仅能修改第一个参数的返回值。 image.png

SyncLoopHook

循环型钩子--SyncLoopHook:执行当前任务时候会进入任务循环执行,如果循环过程中任务返回的结果为undefined,那么就停止当前任务循环重复执行,跳转到下一个任务继续循环执行。 image.png (2) 异步钩子执行机制

二、异步事件流

在异步事件流中按照是否使用promise分类可以分为两类: 使用promise的绑定和触发:tapPromise绑定和promise触发 不使用promise的绑定和触发:tapAsync绑定和callAsync触发

// 使用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');
})

AsyncParallelHook

这里的parallel是指并行的意思 1.通过tapAsync注册,通过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

2.tapPromise注册和promise触发

// 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 实例结果都成功,会将结果存储在数组中,并作为参数传递给 promise 的 then 方法中成功的回调,如果有一个失败就是将失败的结果返回作为参数传递给失败的回调。

AsyncParallelBailHook

AsyncSeriesHook

这里的series是指串行的意思

// 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

tapPromise/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' ]

AsyncSeriesBailHook

AsyncSeriesWaterfallHook

在上面 Async 异步类型的 “钩子中”,我们只着重介绍了 “串行” 和 “并行”(AsyncParallelHook 和 AsyncSeriesHook)以及回调和 Promise 的两种注册和触发事件的方式,还有一些其他的具有一定特点的异步 “钩子” 我们并没有进行分析,因为他们的机制与同步对应的 “钩子” 非常的相似。

AsyncParallelBailHook 和 AsyncSeriesBailHook 分别为异步 “并行” 和 “串行” 执行的 “钩子”,返回值不为 undefined,即有返回值,则立即停止向下执行其他事件处理函数,实现逻辑可结合 AsyncParallelHook 、AsyncSeriesHook 和 SyncBailHook。

AsyncSeriesWaterfallHook 为异步 “串行” 执行的 “钩子”,上一个事件处理函数的返回值作为参数传递给下一个事件处理函数,实现逻辑可结合 AsyncSeriesHook 和 SyncWaterfallHook。

参考文献:
juejin.cn/post/701886… www.codenong.com/j5e551f7951…