Tapable

Why Tapable

前端开发中 Webpack 本质上是基于事件流的运行机制,它的工作流程是将特定的任务分发到指定的事件钩子中去完成。而实现这一切的核心就是 tapable,Webpack 中的两个基础模块:负责编译的 Compiler 和负责创建 bundle 的 Compilation 都是 tapable 构造函数的实例。


在 Webpack 4.0 的源码中会看到下面这些以 Sync、Async 开头,以 Hook 结尾的方法,这些都tapable 核心库的构造类,它为我们提供不同的事件流机制:


  • SyncBailHook:同步执行,前一步返回是 undefined 才会进入下一个函数,否则直接结束
  • SyncWaterfallHook:同步执行,前一个函数的执行结果作为下一个函数的参数传入
  • SyncLoopHook:同步执行每个函数,若某个函数返回不为 undefined 则继续循环执行该函数,直至该函数返回 undefined 再进入下一个函数
  • AsyncParallelHook:异步并行执行,知道所有异步函数执行结束再进入最后的 finalCallback
  • AsyncParallelBailHook:异步并行执行,只要监听函数的返回值不为 undefined,就会忽略后面的监听函数执行,直接跳跃到callAsync等触发函数绑定的回调函数,然后执行这个被绑定的回调函数
  • AsyncSeriesHook:异步串行执行,函数参数都来自于最初传入的参数
  • AsyncSeriesBailHook:异步串行执行,只要监听函数的返回值不为 undefined,就会忽略后面的监听函数执行,直接跳跃到callAsync等触发函数绑定的回调函数,然后执行这个被绑定的回调函数
  • AsyncSeriesWaterfallHook:异步串行执行,上一个监听函数的中的callback(err, data)的第二个参数,可以作为下一个监听函数的参数

Tapable and EventEmitter

Tapable 和 EventEmitter 都是实现了 事件的订阅与发布 功能,很多刚接触Tapable的同学可能会懵逼,这玩意和 EventEmitter 有什么区别呢?

  • tapable 在创建订阅中心时需要指定回调函数的参数列表
  • tapable 触发事件时不需要指定事件名,所有的事件都会被调用
// SyncHook 钩子的使用
const { SyncHook } = require("tapable"); // 创建实例
let syncHook = new SyncHook(["name"]); // 注册事件
syncHook.tap("login", (name) => console.log(name)); // gaollard
syncHook.tap("register", (name) => console.log(name)); // gaollard // 触发事件
syncHook.call("gaollard");
// 引入 events 模块
const events = require('events'); // 创建 eventEmitter 对象
const userEvent = new events.EventEmitter(); userEvent.addListener('login', function(name) {
console.log(name)
}) userEvent.addListener('register', function(name) {
console.log(name) // 打印 gaollard
}) userEvent.emit('login', 'gaollard')

Sync 类型钩子

  • 注册事件 tap
  • 触发事件 call

SyncHook

SyncHook 为串行同步执行,什么都不需要关心,在触发事件之后,会按照事件注册的先后顺序执行所有的事件处理函数,参数就是调用call传入的参数:

// SyncHook 钩子的使用
const { SyncHook } = require("tapable"); // 创建实例 ["name"] 用于声明回调函数的参数个数
let userSyncHook = new SyncHook(["name"]); // 注册事件 第一个参数为事件名, 第二个参数为注册的回调函数
userSyncHook.tap("login", (name) => console.log(name));
userSyncHook.tap("register", (name) => console.log(name)); // 触发事件
userSyncHook.call("gaollard");
console.log(userSyncHook);


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

SyncBailHook

SyncBailHook 为串行同步执行,如果事件处理函数执行时有一个返回值不为 undefined,则跳过剩下未执行的事件处理函数:

// 创建实例
let userSyncHook = new SyncBailHook(["name"]); // 注册事件
userSyncHook.tap("login", (name) => {
console.log(name)
return null // 返回值不为 undefined
}); userSyncHook.tap("register", (name) => {
console.log(name)
}); // 触发事件,让监听函数执行
userSyncHook.call("gaollard"); // 只会打印一次

SyncWaterfallHook

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

// 创建实例
let userSyncHook = new SyncWaterfallHook(["name"]); // 注册事件
userSyncHook.tap("login", (name) => {
console.log('login', name) // 打印 gaollard
}); userSyncHook.tap("register", (name) => {
console.log('register', name) // login回调未返回值, 所以参数为 "gaollard"
return "hello"
}); userSyncHook.tap("enroll", (name) => {
console.log("enroll", name) // register回调返回"hello", 所以参数为 "hello"
}); // 触发事件
userSyncHook.call("gaollard");

SyncLoopHook

SyncLoopHook 为串行同步执行,但是 SyncLoopHook 中的每一个事件回调函数都会被循环执行,事件处理函数返回 undefined 表示结束循环,当前的事件回调循环结束后进入到下一个回调函数中,直到整个流程结束:

// 创建实例
let userSyncHook = new SyncLoopHook(["name"]); let num1 = 1 // 注册事件
userSyncHook.tap("login", (name) => {
console.log('login', name, num1)
return (++num1) > 10 ? undefined : true
}); userSyncHook.tap("register", (name) => {
console.log('login', name, num1)
return (++num1) > 20 ? undefined : true
}); // 触发事件
userSyncHook.call("manbax");

卧槽,连 21 也被打印出来了??? 发现了 tapable 一个BUG(写完去github提issue)

Async 类型钩子

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

AsyncParallelHook

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

  • tapAsync/callAsync
const { AsyncParallelHook } = require("tapable");

// 创建实例
let asyncParallelHook = new AsyncParallelHook(["name"]); console.time("time"); // 注册事件
asyncParallelHook.tapAsync("login", (name, done) => {
setTimeout(() => {
console.log("login", name, new Date());
done();
}, 1000);
}); asyncParallelHook.tapAsync("register", (name, done) => {
setTimeout(() => {
console.log("register", name, new Date());
done();
console.timeEnd("time");
}, 2000);
}); // 触发事件, callAsync 的最后一个参数为回调函数,在所有事件处理函数执行完毕后执行。
asyncParallelHook.callAsync("manbax", () => {
console.log("complete");
});


上面的代码中:两个事件处理函数会并行的执行,都执行完成后(done 被调用),触发 callAsync 回调函数。所有 tapAsync 注册的事件处理函数最后一个参数都为一个回调函数 done,每个事件处理函数在异步代码执行完毕后调用 done 函数,则可以保证 callAsync 会在所有异步函数都执行完毕后执行,接下来看一看 callAsync 是如何实现的:

// 模拟 SyncLoopHook 类
class AsyncParallelHook {
constructor(args) {
this.args = args;
this.taps = [];
}
tapAsync(name, task) {
this.taps.push({
name: name,
fn: task,
type: 'sync',
});
}
callAsync(...args) {
if (args.length < this.args.length) {
throw new Error("参数不足");
} let sum = 0
const fn = args.pop();
const params = args.splice(0, this.args.length);
const done = () => {
(++sum === this.taps.length) && fn()
}
this.taps.forEach(task => {
task.fn(params, done)
})
}
}
  • tapPromise/promise

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

const { AsyncParallelHook } = require("tapable");

// 创建实例
let asyncParallelHook = new AsyncParallelHook(["name"]); console.time("time"); // 注册事件
asyncParallelHook.tapPromise("login", (name) => {
return new Promise((resolve, reject) => {
setTimeout(() => {
console.log("login", name, new Date());
resolve();
}, 1000);
})
}); asyncParallelHook.tapAsync("register", (name) => {
return new Promise((resolve, reject) => {
setTimeout(() => {
console.log("register", name, new Date());
resolve();
console.timeEnd("time");
}, 2000);
})
}); // 触发事件
asyncParallelHook.promise("manbax").then(() => {
console.log("complete");
});


AsyncParallelHook 的实现:

class AsyncParallelHook {
constructor(args) {
this.args = args;
this.taps = [];
}
tapPromise(name, task) {
this.taps.push({
name: name,
fn: task,
type: 'async',
});
}
promise(...args) {
if (args.length < this.args.length) {
throw new Error("参数不足");
}
return new Promise.all(this.taps.map(task => task.fn(...args)))
}
}

AsyncParallelBailHook

  • tapPromise/promise
const { AsyncParallelBailHook } = require("tapable");

// 创建实例
let userHook = new AsyncParallelBailHook(["name"]); console.time("time"); // 注册事件
userHook.tapPromise("login", (name) => {
return new Promise((resolve, reject) => {
setTimeout(() => {
console.log("login", name, new Date());
resolve(undefined) // 此处为 undefined 进入到下一个回调
}, 1000);
})
}); userHook.tapPromise("register", (name) => {
return new Promise((resolve, reject) => {
setTimeout(() => {
console.log("register", name, new Date());
resolve("2"); // 这个回调完成后直接触发最后回调
}, 2000);
})
}); userHook.tapPromise("enroll", (name) => {
return new Promise((resolve, reject) => {
setTimeout(() => {
console.log("enroll", name, new Date());
reject("2");
console.timeEnd("time");
}, 3000);
})
}); // 触发事件
userHook.promise("manbax").then((res) => {
console.log("complete", res)
}).catch(err => {
console.log("error", err)
})

AsyncSeriesHook

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

  • tapAsync/callAsync
const { AsyncSeriesHook } = require("tapable");

// 创建实例
let userHook = new AsyncSeriesHook(["name"]); console.time() userHook.tapAsync('login', function(name, done) {
setTimeout(() => {
console.log('login--', name, new Date())
done()
}, 1000)
}) userHook.tapAsync('register', function(name, done){
setTimeout(() => {
console.log('register--', name, new Date())
done()
}, 2000)
}) // 整个调用花费了 3S
userHook.callAsync('manbax', () => {
console.log('complete')
console.timeEnd()
})
  • tapPromise/promise
const { AsyncSeriesHook } = require("tapable");

// 创建实例
let userHook = new AsyncSeriesHook(["name"]); console.time() userHook.tapPromise('login', function(name){
return new Promise((resolve) => {
setTimeout(() => {
console.log('login--', name, new Date())
resolve()
}, 1000)
})
}) userHook.tapPromise('register', function(name){
return new Promise((resolve) => {
setTimeout(() => {
console.log('register--', name, new Date())
resolve()
}, 2000)
})
}) // 整个调用花费了 3S
userHook.promise('manbax').then(res => {
console.log('complete')
console.timeEnd()
})

AsyncSeriesBailHook

const { AsyncSeriesBailHook } = require("tapable");

// 创建实例
let userHook = new AsyncSeriesBailHook(["name"]); console.time() userHook.tapAsync('login', function(name, done) {
setTimeout(() => {
console.log('login--', name, new Date())
done(1) // 这里返回1, 第二个不会执行(register)
}, 1000)
}) userHook.tapAsync('register', function(name, done){
setTimeout(() => {
console.log('register--', name, new Date())
done(2)
}, 2000)
}) // 整个调用花费了 3S
userHook.callAsync('manbax', (_, data) => {
console.log('complete')
console.timeEnd()
})

AsyncSeriesWaterfallHook

  • tapAsync/callAsync: tapAsync 中的 done 回调函数需要传入两个参数,第一个表示是否有异常,第二个为返回值。
const { AsyncSeriesWaterfallHook } = require("tapable");

// 创建实例
let userHook = new AsyncSeriesWaterfallHook(["name"]); console.time() userHook.tapAsync('login', function(name, done) {
setTimeout(() => {
console.log('login--', name, new Date())
done(null, "1")
}, 1000)
}) userHook.tapAsync('register', function(name, done){
setTimeout(() => {
console.log('register--', name, new Date())
done(null, "2")
}, 2000)
}) // 整个调用花费了 3S
userHook.callAsync('manbax', (_, data) => {
console.log('complete', data)
console.timeEnd()
})

API模拟实现

SyncHook

// 模拟 SyncHook 类
class MySyncHook {
constructor(args) {
this.args = args;
this.taps = [];
}
tap(name, task) {
this.taps.push({
name: name,
fn: task,
type: 'sync',
});
}
call(...args) {
if (args.length < this.args.length) {
// 参数不足时抛出异常
throw new Error("参数不足");
} // 参数长度与创建实例传入数组长度一直,不足补 undefined
// 因为长度不足时已经抛出异常,故注释
// args = args.slice(0, this.args.length); // 依次执行事件处理函数
this.taps.forEach(task => task.fn(...args));
}
}

SyncBailHook

// 模拟 SyncBailHook 类
class SyncBailHook {
constructor(args) {
this.args = args;
this.taps = [];
}
tap(name, task) {
this.taps.push({
name: name,
fn: task,
type: 'sync',
});
}
call(...args) {
if (args.length < this.args.length) {
throw new Error("参数不足");
} let i = 0, res;
do {
res = this.taps[i++].fn(...args)
} while (res === undefined && i < this.taps.length)
}
}

SyncWaterfallHook

// 模拟 SyncWaterfallHook 类
class SyncWaterfallHook {
constructor(args) {
this.args = args;
this.taps = [];
}
tap(name, task) {
this.taps.push({
name: name,
fn: task,
type: 'sync',
});
}
call(...args) {
if (args.length < this.args.length) {
throw new Error("参数不足");
} return this.taps.reduce((res, current) => {
let _res = current.fn(res)
// 若当前的回调函数没有返回值,那么就使用上一个参数
return _res !== undefined ? _res : res
}, ...args)
}
}

SyncLoopHook

// 模拟 SyncLoopHook 类
class SyncLoopHook {
constructor(args) {
this.args = args;
this.taps = [];
}
tap(name, task) {
this.taps.push({
name: name,
fn: task,
type: 'sync',
});
}
call(...args) {
if (args.length < this.args.length) {
throw new Error("参数不足");
} let i = 0
while (i < this.taps.length) {
const task = this.taps[i++].fn const magic = function () {
let res = task(...args)
if (res !== undefined) {
magic()
}
};
magic();
}
}
}

AsyncSeriesHook

class AsyncSeriesHook {
constructor(args) {
this.args = args;
this.taps = [];
}
tapAsync(name, task) {
this.taps.push({
name: name,
fn: task,
});
}
callAsync(...args) {
if (args.length < this.args.length) {
throw new Error("参数不足");
} let i = 0
const cb = args.pop()
const _args = args.splice(0, args.length) const next = () => {
const task = this.taps[i++]
if (task) {
task.fn(..._args, next)
} else {
cb()
}
}
next()
}
}

AsyncSeriesWaterfallHook

 class AsyncSeriesWaterfallHook {
constructor() {
this.tasks = [];
} tap(name, task) {
this.tasks.push(task);
} call(...args, finalCb) {
let count = 0;
const len = this.tasks.length;
const next = (err, data) => {
if(count === len) return finalCb()
let task = this.tasks[count];
if (count === 0) {
task(...args, next);
} else {
task(data, next);
}
count++;
};
next() }
}

总结

仔细思考发现 Tapable 事件机制 就像工厂里面生产线:

  • 前序工位的输出是后序工位的输入
  • 当某个产品在流产线上的工位发生异常时,这个产品的后序流程终止

它非常适合用于解决流水作业,就像 Webpack 对文件进行处理正是这样的场景。学习 tapable 有助于帮助我们更高的理解 Webpack。


tapable的注册事件的方法有:tab/tapSync/tapPromise 和触发事件的方法 call/callAsync/promise,在 Webpack 中,我们通过这些API来设计钩子,这些 “钩子” 能够将 Webpack 中插件/加载器/功能独立的模块连接起来,以减少耦合性和提高扩展性。

从 Tapable 中得到的启发的更多相关文章

  1. 这才是官方的tapable中文文档

    起因 搜索引擎搜索tapable中文文档,你会看见各种翻译,点进去一看,确实是官方的文档翻译过来的,但是webpack的文档确实还有很多需要改进的地方,既然是开源的为什么不去github上的tapab ...

  2. webpack4.0各个击破(8)—— tapable篇

    webpack作为前端最火的构建工具,是前端自动化工具链最重要的部分,使用门槛较高.本系列是笔者自己的学习记录,比较基础,希望通过问题 + 解决方式的模式,以前端构建中遇到的具体需求为出发点,学习we ...

  3. Photoshop中的高斯模糊、高反差保留和Halcon中的rft频域分析研究

    在Halcon的rft变换中,我们经常可以看到这样的算子组合: rft_generic (Image, ImageFFT2, 'to_freq', 'none', 'complex', Width) ...

  4. Tapable 0.2.8 入门

    [原文:Tapable 0.2.8 入门] tapable是webpack的核心框架(4.0以上版本的API已经发生了变化),是一个基于事件流的框架,或者叫做发布订阅模式,或观察者模式,webpack ...

  5. IntelliJ IDEA+SpringBoot中静态资源访问路径陷阱:静态资源访问404

    IntelliJ IDEA+SpringBoot中静态资源访问路径陷阱:静态资源访问404 .embody{ padding:10px 10px 10px; margin:0 -20px; borde ...

  6. webpack4核心模块tapable源码解析

    _ 阅读目录 一:理解Sync类型的钩子 1. SyncHook.js 2. SyncBailHook.js 3. SyncWaterfallHook.js 4. SyncLoopHook.js 二: ...

  7. webpack4.0源码分析之Tapable

    1 Tapable简介 webpack本质上是一种事件流的机制,它的工作流程就是将各个插件串联起来,而实现这一切的核心就是Tapable,webpack中最核心的负责编译的Compiler和负责创建b ...

  8. Webpack 核心模块 tapable 解析(转)

        原文出自:https://www.pandashen.com 前言 Webpack 是一个现代 JavaScript 应用程序的静态模块打包器,是对前端项目实现自动化和优化必不可少的工具,We ...

  9. 将Lambda表达式作为参数传递并解析-在构造函数参数列表中使用Lambda表达式

    public class DemoClass { /// <summary> /// 通过Lambda表达式,在构造函数中赋初始值 /// </summary> /// < ...

随机推荐

  1. Vuex原理实现

    Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式.它采用集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化. 思考问题 Vuex 只在更实例引入了,那么 ...

  2. Mysql查询语句,select,inner/left/right join on,group by.....[例题及答案]

    创建如下表格,命名为stu_info, course_i, score_table. 题目: 有如图所示的三张表结构,学生信息表(stu_info),课程信息表(course_i),分数信息表(sco ...

  3. Java实现派(Pie, NWERC 2006, LA 3635)

    题目 有F+1个人来分N个圆形派,每个人得到的必须是一整块派,而不是几块拼在一起,且面积要相同.求每个人最多能得到多大面积的派(不必是圆形). 输入的第一行为数据组数T.每组数据的第一行为两个整数N和 ...

  4. Java实现 LeetCode 303 区域和检索 - 数组不可变

    303. 区域和检索 - 数组不可变 给定一个整数数组 nums,求出数组从索引 i 到 j (i ≤ j) 范围内元素的总和,包含 i, j 两点. 示例: 给定 nums = [-2, 0, 3, ...

  5. Java实现 LeetCode 75 颜色分类

    75. 颜色分类 给定一个包含红色.白色和蓝色,一共 n 个元素的数组,原地对它们进行排序,使得相同颜色的元素相邻,并按照红色.白色.蓝色顺序排列. 此题中,我们使用整数 0. 1 和 2 分别表示红 ...

  6. Java动态规划实现最短路径问题

    问题描述 给定一个加权连通图(无向的或有向的),要求找出从每个定点到其他所有定点之间的最短路径以及最短路径的长度. 2.1 动态规划法原理简介 动态规划算法通常用于求解具有某种最优性质的问题.在这类问 ...

  7. 阿里巴巴 《Java 开发者手册》+ IDEA编码插件

    4月22日,阿里巴巴发布了泰山版<Java 开发手册>,以前以为终极版就真的是终极版了,没想到还是想的太简单了,继终极版之后又发布了详尽版.华山版,这不,泰山版又来了.想想也对,行业一直在 ...

  8. HttpClientFactory-向外请求的最佳

    简介 它的组件包是Microsoft.Extensions.Http 复原HttpClient带来的问题 HttpClient相关问题 虽然HttpClient类实现了IDisposable,但不是首 ...

  9. 第二个hibernate Annotation版本的helloworld

    经过第一次的 hibernate  我发现每一个数据库表都对应了一个类,并且每一个类都要新建一个文件进行配置 很麻烦!  于是便出现了Annotation版本的hibernate. 具体如下: 1.同 ...

  10. k8s学习-Service

    4.4.Service 可能会用到ipvs,先安装: yum install -y openssl openssl-devel popt popt-devel libnl-devel kenel-de ...