订阅
在我们的生活中,订阅的现象随处可见,比如我们到书店里要买一本书,但目前并没有到货,我们到柜台前留下信息,店长在到货后变通过我们留下的信息来通知我们货到了,这便是订阅的过程。
发布订阅模式
1.你去买东西断货了, 老板让你留下联系方式, 到货了给你打电话, 这就是发布订阅模式
2.老板就是发布者, 你就是订阅者
3.订阅者将电话添加到了发布者的缓存列表中(电话簿)
4.当发布者到货后就会遍历缓存列表依次通知所有订阅者
实现
// 定义一个类作为发布者
class SyncHook {
constructor(args){
// 缓存列表
this.tasks = [];
// 定义属性保存将来会给订阅者传递多少个参数
this.args = args;
}
// 用于订阅的方法
tap(tag, task){
this.tasks.push(task);
}
// 用于发布的方法
call(...args){
if(args.length < this.args.length){
return new Error("参数个数不对");
}
// 剔除多余的参数
args = args.slice(0, this.args.length);
this.tasks.forEach(function (task) {
task(...args);
})
}
}
module.exports = SyncHook;
接下来我们创建一个发布者:
// 1.创建发布者
let hook = new SyncHook(["name", "price"]);
// 2.订阅者像发布者订阅
hook.tap("zs", function (name, price) {
console.log(name, price);
});
hook.tap("ls", function (name, price) {
console.log(name, price);
});
hook.tap("ww", function (name, price) {
console.log(name, price);
});
最后我们发布消息:
// 3.发布者发布消息
hook.call("豪车", 88888, 666);
实现原理很简单,就是创建一个发布者类,在其中创建一个列表用于保存订阅者的信息及默认传入参数的个数,接下来对齐遍历赋值即可。
Tapable
这是一个发布订阅模式的插件,它实现了我们上面的功能,并且还拥有众多在不同需求下的功能。
这些是它的配置:
const {
SyncHook,
SyncBailHook,
SyncWaterfallHook,
SyncLoopHook,
AsyncParallelHook,
AsyncParallelBailHook,
AsyncSeriesHook,
AsyncSeriesBailHook,
AsyncSeriesWaterfallHook
} = require("tapable");
这些便是它内置的发布者。
SyncHook同步串行钩子:
SyncHook不关心订阅函数(事件处理函数)的返回值,在收到消息(触发事件)之后,
会按照订阅的先后顺序执行所有的订阅函数(事件处理函数)
SyncBailHook同步串行钩子:
SyncBailHook关心订阅函数(事件处理函数)的返回值,在收到消息(触发事件)之后,
会按照订阅的先后顺序执行所有的事件处理函数
如果你查阅webpack底层源码, 你会发现webpack内部其实是由大量的插件构成的。Webpack本质上是一种事件流的机制,它的工作流程就是将各个插件串联起来,而实现这一切的核心就是 tapable. 所以说不懂 tapable 就看不懂 Webpack 源码。
并且在前端开发中发布订阅模式的使用频率非常高。
例如: webpack、Vue、React、Angular等各大框架都在用
我们选取三个来进行说明:
SyncHook
const { SyncHook } = require('tapable');
class Lesson {
constructor() {
this.hooks = {
// 创建一个发布者对象
vue: new SyncHook(["des"]),
}
}
tap(){
// 订阅消息
this.hooks.vue.tap("zs", function (des) {
console.log("zs", des);
});
this.hooks.vue.tap("ls", function (des) {
console.log("ls", des);
});
}
call(){
// 发布消息
this.hooks.vue.call("书到了");
}
}
let ls = new Lesson();
ls.tap();
ls.call();
按照订阅的先后顺序执行所有的订阅函数(事件处理函数)。
SyncBailHook
const { SyncBailHook } = require('tapable');
class Lesson {
constructor() {
this.hooks = {
// 创建一个发布者对象
vue: new SyncBailHook(["des"]),
}
}
tap(){
// 订阅消息
this.hooks.vue.tap("zs", function (des) {
console.log("zs", des);
// return "1";
return undefined;
});
this.hooks.vue.tap("ls", function (des) {
console.log("ls", des);
// return "2";
});
this.hooks.vue.tap("ww", function (des) {
console.log("ww", des);
return "3";
});
}
call(){
// 发布消息
this.hooks.vue.call("书到了");
}
}
let ls = new Lesson();
ls.tap();
ls.call();
会按照订阅的先后顺序执行所有的事件处理函数,并且如果在执行的过程中有一个订阅函数返回的不是undefined, 就会停止执行后续函数。
SyncWaterfallHook
const { SyncWaterfallHook } = require('tapable');
class Lesson {
constructor() {
this.hooks = {
// 创建一个发布者对象
vue: new SyncWaterfallHook(["des"]),
}
}
tap(){
// 订阅消息
this.hooks.vue.tap("zs", function (des) {
console.log("zs", des);
return "1";
});
this.hooks.vue.tap("ls", function (des) {
console.log("ls", des);
return "2";
});
this.hooks.vue.tap("ww", function (des) {
console.log("ww", des);
return "3";
});
}
call(){
// 发布消息
this.hooks.vue.call("书到了");
}
}
let ls = new Lesson();
ls.tap();
ls.call();
会按照订阅的先后顺序执行所有的事件处理函数,并且会将上一个订阅函数的返回值作为参数传递给下一个订阅函数。
AsyncParallelHook
const { AsyncParallelHook } = require('tapable');
class Lesson {
constructor() {
this.hooks = {
// 创建一个发布者对象
vue: new AsyncParallelHook(["des"]),
}
this.index = 0;
}
tap(){
// 订阅消息
this.hooks.vue.tapAsync("zs", (des, cb) => {
setTimeout(function () {
console.log("zs", des);
cb();
}, 3000);
});
this.hooks.vue.tapAsync("ls", function (des, cb) {
setTimeout(function () {
console.log("ls", des);
// cb();
}, 2000);
});
this.hooks.vue.tapAsync("ww", function (des, cb) {
setTimeout(function () {
console.log("ww", des);
cb();
}, 1000);
});
}
call(){
// 发布消息
this.hooks.vue.callAsync("书到了", function () {
console.log("end");
});
}
}
let ls = new Lesson();
ls.tap();
ls.call();
发出消息后,会同时执行所有的订阅函数,并且在执行订阅函数时会自动传递一个callback参数。
每个订阅函数执行完毕之后必须通过callback告诉系统订阅函数已经执行完毕了,当所有订阅函数都调用完callback后会通过回调函数的方式告callAsync全部执行完毕了。
(注意:若是异步并行钩子,订阅方法要使用tapAsync,发布方法要使用callAsync,即在tap和call方法后加上Async以示区分)
发布订阅模式的编写,插件tapable的使用至此结束。原理和使用很简单,也很重要,正如前面所说,很多框架都用到了它,这是编写插件的必备技能。