需求
最近在项目中遇到一个需求,要在点击按钮保存的时候同时去触发多个组件中的数据更新,原本项目中是用 vuex 实现了一个发布订阅的功能的,但是由于是用到 vuex 管理状态的功能,对于更新状态可能还方便,要触发调用某些方法函数可能用起来就有点别扭了。所以参考网上的一些资料,简单的做了一个发布订阅模块。
发布订阅模式
发布订阅模式也称为观察者模式,是一种用于处理对象及其行为和状态之间的关系的设计模式。生活中比较常见的“发布-订阅模式”例如快递:当我们在网上买东西,付款,填上电话号码后会产生一个快递单号,这个就相当于我们订阅的操作,之后关于这个快递单号对应快递的所有信息都会及时的传递给我们,当快递到了之后,快递员会打电话通知我们去取快递,这就相当于发布操作,由此构成一个事件闭环,其实浏览器的 DOM 事件等就是一种发布订阅,给元素绑定事件相当于订阅,触发事件相当于发布,所以发布订阅模式可以说是让我们在 DOM 元素以外的对象或场景使用事件模型的一种设计模式。
实现一个简单的发布订阅模式
class SimplePubSub {
constructor() {
// 订阅数据清单
this.subList = {};
}
// 订阅
subscribe(subName, fn) {
const isFunction = typeof fn === "function";
const isFunctionArray = Array.isArray(fn);
// 如果fn既不是函数也不是函数数组
if (!isFunction && !isFunctionArray) {
return console.error(fn + " must be a function or function array!");
}
// 如果订阅已经存在
if (Array.isArray(this.subList[subName])) {
// 如果fn是函数数组
if (isFunctionArray) {
this.subList[subName].push(...fn);
} else {
this.subList[subName].push(fn);
}
} else {
if (isFunctionArray) {
this.subList[subName] = fn;
} else {
this.subList[subName] = [fn];
}
}
}
// 发布
publish(...args) {
const subName = args.shift();
const currentSubList = this.subList[subName];
const len = currentSubList.length;
if (len) {
for (let i = 0; i < len; i++) {
const param = args[i];
// 如果存在对应位置的参数则使用参数调用
if (param) {
currentSubList[i](param);
} else {
currentSubList[i]();
}
}
}
}
// 移除
remove(subName, fn) {
const fns = this.subList[subName];
if (!fns) {
console.error("Can't find the " + subNames);
return;
}
// 清除对应订阅的所有处理函数
if (!fn) {
delete this.subList[subName];
return;
}
// 清除指定订阅的指定处理函数
for (let i = 0, len = fns.length; i < len; i++) {
if (fns[i] === fn) {
fns.splice(i, 1);
// 如果处理函数都清除了直接删除对应订阅
if (!fns.length) {
delete this.subList[subName];
return;
}
}
}
}
}
// 使用
const pubSub = new SimplePubSub();
pubSub.subscribe("click", (msg) => {
console.log(msg);
});
pubSub.subscribe("funArray", [
(msg) => {
console.log(msg + " fun1");
},
(msg) => {
console.log(msg + " fun2");
},
(msg) => {
console.log(msg + " fun3");
},
]);
pubSub.publish("click", "Button click!");
// output: Button click!
pubSub.publish("funArray", "fun1's param", "fun2's param", "fun3's param");
// output:
// fun1's param fun1
// fun2's param fun2
// fun3's param fun3
添加单例
为了保证全局使用同一个发布订阅对象,我们应该为其添加单例模式以保证如果已经创建过一个发布订阅对象,之后在任何地方再次创建都直接返回已创建的对象。如果用 ES6 的导出语法 export 来导出模块,并用 import 来导入,则可以不必再加单例模式,因为 import 的导入本身已经是单例模式,多次 import 同一个模块只会引入一次对应的实例。
如果不采用以上的方法,那么我们可以自行添加单例模式,其实也不难。首先每次 new 的时候都会执行一次构造函数 constructor,所以我们可以在构造函数里面做些处理:
// ···前略
constructor() {
if (SimplePubSub.instance) {
return SimplePubSub.instance
}
// 订阅数据清单
this.subList = {};
SimplePubSub.instance = this
}
// 后略
总结
发布订阅模式以及单例模式在 js 中是比较常用的且非常有用的两种设计模式,很多场景都有他们的身影,所以掌握他们会对我们日常的开发工作提供很大的帮助。目前网上有非常多的相关的教程都可以供我们学习参考,本文仅仅是我自己在开发中的一点思路,记录一下以便自己日后复习和完善。当然由于自己能力有限,文中可能有些不对的地方,欢迎指正,一起进步。