发布订阅模式(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, ...argsemit() {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);}}
