发布订阅模式(publish-subscribe patterns),又称为观察者模式。
浏览器的事件池机制,也是通过这个模式来完成的。
订阅就是添加事件处理程序
发布就是事件触发,有个观察者来监听事件是否触发,以及触发后如何执行事件处理程序
取消订阅就是移除事件处理程序
思想:
准备一个容器,把到达某个条件要处理的事情,事先一一添加到容器中
当到达指定时间点,通知容器中的方法依次执行即可
1. 简单的发布订阅
分为订阅、发布、取消订阅三个步骤
let ary = []; //=> 订阅名单
//=> 订阅
function subscribe(f) {
let n = ary.indexOf(f);
n == -1 ? ary.push(f) : null; //=> 就是把人加入名单中
}
//=> 发布
function publish() {
//=> 遍历所有订阅人,执行对应函数
ary.forEach((item) => {
item && item();
})
}
//=> 取消订阅
function off(f) {
//=> 移除该订阅人
let n = ary.indexOf(f);
if (n > -1) {
ary.splice(n, 1);
}
}
2. 具体的发布订阅
指定特定的报社(元素),以及一个具体的类型(也就是特定杂志),以这两个属性来分开管理分别订阅发布
function subscribe(ele, type, f) {
//=> 把之前定义到外面的名单放到元素的特定属性上,
ele[type].ary = ele[type].ary || [];
let n = ele[type].ary.indexOf(f);
n == -1 ? ele[type].ary.push(f) : null; //=> 就是把人加入名单中
}
//=> 发布
function publish(ele, type) {
//=> 遍历所有订阅人,执行对应函数
ele[type].ary = ele[type].ary || [];
ele[type].ary.forEach((item) => {
item && item();
})
}
//=> 取消订阅
function off(ele, type, f) {
ele[type].ary = ele[type].ary || [];
//=> 移除该订阅人
let n = ele[type].ary.indexOf(f);
if (n != -1) {
ele[type].ary.splice(n, 1);
}
}
3. 面向对象的发布订阅
class Subscribe {
constructor() {
//=> 创建一个容器,管理需要执行的方法
this.pond = [];
}
//=> 向容器添加方法 fn,需要去重
add(fn) {
let n = this.pond.indexOf(fn);
if (n > -1) {
this.pond.push(fn);
}
}
//=> 执行容器中所有的方法
fire(...arg) {
this.pond.forEach((item) => {
item && item(...arg);
})
}
//=> 从容器中移除
remove(fn) {
let n = this.pond.indexOf(fn);
if (n > -1) {
this.pond.splice(n, 1);
}
}
}
4. 加强面向对象的发布订阅
上面的例子只能创建一个订阅名单,如果需要多个,那么就需要多次执行构造函数。
改进 ponds 为一个对象,可以容纳多个具名池子,通过 type 属性获取 三个行为都需要增加一个 type 参数,来操作具体的池子。
同时,增加一个只订阅一次的方法 once、获取某个池子中所有订阅者的方法 listeners、增加类型判断等容错机制。
/**
* Subscribe: 订阅发布模式类
*
* @constructor
* ponds: 事件池 {type: [...]}
* @function
* on(type, listener)
* once(type, listener)
* off(type, listener)
* emit(type, ...args)
* listeners(type)
*
*/
class Subscribe {
constructor() {
//=> []创建一个容器,管理需要执行的方法
//=> {} 实现多个不同类型容器
this.ponds = {};
}
//=> 订阅
on(type, listener) {
// listener 必须是函数
if (typeof listener !== "function") throw new error("second param must be a function");
this.ponds[type] = this.ponds[type] || [];
// 判断事件池中是否已存在相同的 listener,存在则不添加
let n = this.ponds[type].indexOf(listener);
if (n === -1) {
this.ponds[type].push(listener);
}
return this;
}
//=> 订阅一次
once(type, listener) {
if (typeof listener !== "function") throw new error("second param must be a function");
let that = this;
var fn = function () {
listener.apply(that, arguments);
that.off(type, fn);
}
return this.on(type, fn);
}
//=> 执行容器中所有的方法
// 参数为 type, ...args
emit() {
var listeners = this.ponds[type];
if (!listeners) return;
var args = [];
Array.prototype.forEach.call(arguments, function (item) {
args.push(item);
})
var type = args.shift();
// 锁死队列,防止事件池中的函数不断向事件池添加订阅,出现死循环
listeners = listeners.slice();
// 进行逐个发布
listeners.forEach(function(item) {
item.apply(this, args);
})
return this;
}
//=> 取消订阅
off(type, listener) {
var listeners = this.ponds[type]
if (!listeners) return this;
for (let i = 0; i < listeners.length; i++) {
if (listeners[i] === listener) {
listeners.splice(i, 1);
break;
}
}
if (listeners.length === 0) {
delete this.ponds[type]; // 防止空的时候还进行遍历判断
}
return this;
}
//=> 获取所有的订阅者
listeners(type) {
// 返回克隆数组
return (this.ponds[type] || []).slice();
}
}
5. 包含事件的订阅发布
判断是自己的方法还是事件,自己的订阅名单,就创建自己的容器,并将方法放入容器中,如果是事件,就只需要添加到事件池即可,
function on(ele, type, f) {
if (/^my/.test(type)) { //若是自己的事件,就走自己的发布订阅
ele[type] = ele[type] || [];
var n = ele[type].indexOf(f);
if (n == -1) {
ele[type].push(f)
}
} else {
type = type.replace(/^on/g, '');
ele.addEventListener(type, f, false);
}
}
function fire(ele, type) {
ele[type] = ele[type] || [];
ele[type].forEach((item) => {
item && item.call(ele);
})
}
function off(ele, type, f) {
if (/^my/.test(type)) {
ele[type] = ele[type] || [];
var n = ele[type].indexOf(f);
if (n != -1) {
ele[type].splice(n, 1);
}
} else {
type = type.replace(/^on/, '');
ele.removeEventListener(type, f, false);
}
}