从vueBus到观察者
文 / 三和小钢炮
本文所要阐述的知识有以下
- 面向对象编程
- js模块化多规范兼容
- vue的插件编写
- js分层编程
- 观察者模式
- 生产者/消费者
前言
如果没有vueBus,那就自己写一个。
01 观察者模式
观察者模式(Observer):又被称作发布-订阅模式或者消息机制,定义了一种依赖关系,解决了主体对象与观察者之间功能的耦合。
一个观察者应该具有以下接口:
public interface IObserver {
/**
* 发布信息接口
*/
public void fire();
/**
* 注册信息接口
*/
public void regist();
/**
* 移除信息接口
*/
public void remove();
}
02 实现一个观察者
function Event(type, args) {
this.type = type || 'default';
this.args = args || {};
}
function Observer() {
var _msg = {};
this.regist = function(type, fn) {
if (typeof _msg[type] === 'undefined') {
_msg[type] = [fn];
} else {
_msg[type].push(fn);
};
};
this.fire = function(type, args) {
if (typeof _msg[type] === 'undefined') {
return;
};
for (var i = 0, len = _msg[type].length; i < len; i++) {
_msg[type][i].call(this, new Event(type, args));
};
};
this.remove = function(type, fn) {
if (_msg[type] instanceof Array) {
for (var i = _msg[type].length - 1; i >= 0; i--) {
if (_msg[type][i] === fn) {
_msg[type].splice(i, 1);
};
};
};
};
};
03 适配主流js规范
任何一个插件都要适应于主流规范
常用的规范有:CommonJS、CMD、AMD
我们做以下适配,保证它在任何环境下都可以正常运行。
Observer.js文件如下:
/* eslint-disable */
;(function(global, factory) {
if (typeof module !== 'undefined') {
// nodeJs端
module.exports = factory();
} else if (typeof define === 'function') {
if (define.amd) {
// amd规范
define(factory);
} else if (define.cmd) {
// cmd规范
define(function(require, exports, module) {
module.exports = factory();
});
}
} else {
global.Observer = factory();
}
})(this, function () { 'use strict';
function Event(type, args) {
this.type = type || 'default';
this.args = args || {};
}
function Observer() {
// 此消息应该为私有变量
// 考虑到_msg不对外暴露,不采用prototype
var _msg = {};
this.regist = function(type, fn) {
if (typeof _msg[type] === 'undefined') {
_msg[type] = [fn];
} else {
_msg[type].push(fn);
};
};
this.fire = function(type, args) {
if (typeof _msg[type] === 'undefined') {
return;
};
for (var i = 0, len = _msg[type].length; i < len; i++) {
_msg[type][i].call(this, new Event(type, args));
};
};
this.remove = function(type, fn) {
if (_msg[type] instanceof Array) {
for (var i = _msg[type].length - 1; i >= 0; i--) {
if (_msg[type][i] === fn) {
_msg[type].splice(i, 1);
};
};
};
};
};
return new Observer();
});
04 包装成vue插件
封装任何一个工具,最忌讳的就是和其他第三方耦合,大凡是依赖es6、jquery、vue等等,我们只能称之为组件或者插件。所以要把核心模块和其他适配模块抽离,核心模块不应该依赖任何东西。
这里,我们编写一个vue的包裹层,这里我们只需要保证它能够在vuejs里面适用。
ObserverServer.js文件:
/* eslint-disable */
/**
* vue适配层
*/
import Observer from './Observer.js';
const ObserverServer = {};
ObserverServer.install = function (Vue, options) {
Vue.Observer = Observer;
Vue.prototype.$Observer = Observer;
}
export default ObserverServer;
05 在vue中使用
经过上面的操作,我们实际上是实现了一个vueBus,我们可以如下使用:
index.js文件:
import Observer from '~js/observer/ObserverServer.js';
Vue.use(Observer);
a.vue文件:
this.$Observer.regist('type1', function(msg) {
console.log(msg);
});
this.$Observer.regist('type2', function(msg) {
console.log(msg);
});
this.$Observer.fire('type1', '你好');
this.$Observer.fire('type2', 'hello');
06 问题
上面的观察者,看似完美,实际上存在一个非常严重的问题:
注册和发布的加载次序问题
在js中经常存在下面的情况:
注册往往会在发布之后
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Document</title>
<script src="./Observer.js"></script>
</head>
<body>
<button onclick="fn()">点击</button>
<script>
Observer.fire('type1', '你好');
function fn() {
Observer.regist('type1', function (msg) {
console.log(msg);
});
};
</script>
</body>
</html>
其实我们可以寻求下面的解决:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Document</title>
<script src="./Observer.js"></script>
</head>
<body>
<button onclick="fn()">点击</button>
<script>
// 采用两次发布,构成一次通信
Observer.regist('type1.before', function(msg) {
Observer.fire('type1', '你好');
});
var isRegist = false;
function fn() {
// 防止多次注册
if (!isRegist) {
Observer.regist('type1', function (msg) {
console.log(msg);
isRegist = true;
});
}
Observer.fire('type1.before', 'ready');
};
</script>
</body>
</html>
07 生产者/消费者
我们上面的设计,是利用一个注册容器,将{type, fn} (消息类型和函数) 装入其中,一旦消息发布,便会回调触发函数。
实际上,上面的设计存在一个问题:
消息发布者和消息注册者都不应该关心消息被如何调用。
我们之前的结构为:
注册者 –> 注册表 <– 发布者
此时发布者需要处理和他不相干的消息,我们可以把消息发布者看作生产者,而注册者则是消费者,于是我们可以做如下调整。
注册者 –> 注册表 <==> 消息列表 <–发布者
注册者只关心到注册表里面注册,发布者只要关心将消息推送到消息列表里面。
至于注册表和消息列表之间的消费关系,用一个中间的消费机制去处理。
此时如果有消息发布未被消费,我们就采取将消息保留,直到被消费为止。
实现逻辑如下:
/* eslint-disable */
(function(global, factory) {
if (typeof module !== 'undefined') {
// nodeJs端
module.exports = factory();
} else if (typeof define === 'function') {
if (define.amd) {
// amd规范
define(factory);
} else if (define.cmd) {
// cmd规范
define(function(require, exports, module) {
module.exports = factory();
});
}
} else {
global.Observer = factory();
}
})(this, function () { 'use strict';
// 消息实体类
function Msg(type, args) {
this.type = type || 'default';
this.args = args || {};
this.time = new Date().getTime();
}
function Observer() {
var _registTable = {}; // 注册表
var _msgQueue = []; // 消息队列
//消费机制
function consume() {
for (var i = 0; i < _msgQueue.length; i++) {
var msg = _msgQueue[i];
var registList = _registTable[msg.type];
if (registList instanceof Array) {
for (var j = 0; j < registList.length; j++) {
registList[j].call(this, msg);
}
_msgQueue.splice(i, 1);
i--;
}
}
}
// 注册
this.regist = function(type, fn) {
if (typeof _registTable[type] === 'undefined') {
_registTable[type] = [fn];
} else {
_registTable[type].push(fn);
};
consume();
};
// 发布
this.fire = function(type, args) {
_msgQueue.push(new Msg(type, args));
consume();
};
// 注销
this.remove = function(type, fn) {
if (_registTable[type] instanceof Array) {
for (var i = _registTable[type].length - 1; i >= 0; i--) {
if (_registTable[type][i] === fn) {
_registTable[type].splice(i, 1);
};
};
};
};
};
return new Observer();
});
这样我们前后依赖的问题就解决了。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Document</title>
<script src="./Observer.js"></script>
</head>
<body>
<button onclick="fn()">点击</button>
<script>
Observer.fire('type1', '你好');
function fn() {
Observer.regist('type1', function (msg) {
console.log(msg);
});
};
</script>
</body>
</html>
08 消费记录
上面的实现的设计想法是:保证消息要全部被消费。
这里又会有一个问题:
<script src="./Observer.js"></script>
<script>
Observer.fire('type1', '你好');
Observer.regist('type1', function (msg) {
console.log('No1', msg);
});
Observer.regist('type1', function (msg) {
console.log('No2', msg);
});
</script>
此时只会有一个注册者能收到消息。因为消息一旦被消费,就会被删除~。
那么我们需要保留每个消息的消费记录,每次消费消息的时候,先去查询是否有消费记录。
代码如下:
/* eslint-disable */
(function(global, factory) {
if (typeof module !== 'undefined') {
// nodeJs端
module.exports = factory();
} else if (typeof define === 'function') {
if (define.amd) {
// amd规范
define(factory);
} else if (define.cmd) {
// cmd规范
define(function(require, exports, module) {
module.exports = factory();
});
}
} else {
global.Observer = factory();
}
})(this, function () { 'use strict';
// 消息实体类
function Msg(type, args) {
this.type = type || 'default';
this.args = args || {};
this.timeId = new Date().getTime() + '_' + Msg.ID++;
}
// 消息id
Msg.ID = 0;
function Observer() {
var _registTable = {}; // 注册表
var _msgQueue = []; // 消息队列
var _consumeHistory = {}; // 消费历史
// 增加消费记录
function addConsumeHistory(fnID, msgID) {
if (typeof _consumeHistory[msgID] === 'undefined') {
_consumeHistory[msgID] = [fnID];
} else {
_consumeHistory[msgID].push(fnID);
};
};
// 查询消费记录
function getConsumeHistory(fnID, msgID) {
var isConsume = false;
if (typeof _consumeHistory[msgID] === 'undefined') return isConsume;
var historyList = _consumeHistory[msgID];
if (historyList.indexOf(fnID) > -1) {
isConsume = true;
}
return isConsume;
};
//消费机制
function consume() {
for (var i = 0; i < _msgQueue.length; i++) {
var msg = _msgQueue[i];
var registList = _registTable[msg.type];
if (registList instanceof Array) {
for (var j = 0; j < registList.length; j++) {
if (!getConsumeHistory(registList[j].registID, msg.timeId)) {
registList[j].call(this, msg);
addConsumeHistory(registList[j].registID, msg.timeId);
}
}
}
}
}
function regist(type, fn) {
if (typeof _registTable[type] === 'undefined') {
_registTable[type] = [fn];
} else {
_registTable[type].push(fn);
};
fn.registID = regist.ID++;
}
// 注册id
regist.ID = 0;
// 注册
this.regist = function(type, fn) {
regist(type, fn);
consume();
};
// 发布消息
this.fire = function(type, args) {
_msgQueue.push(new Msg(type, args));
consume();
};
// 注销
this.remove = function(type, fn) {
if (_registTable[type] instanceof Array) {
for (var i = _registTable[type].length - 1; i >= 0; i--) {
if (_registTable[type][i] === fn) {
_registTable[type].splice(i, 1);
};
};
};
};
};
return new Observer();
});
09 消息过期
运行上面的代码
<script src="./Observer.js"></script>
<script>
Observer.fire('type1', '你好');
Observer.regist('type1', function (msg) {
console.log('No1', msg);
});
Observer.regist('type1', function (msg) {
console.log('No2', msg);
});
</script>
上面就能收到两条消息。
那么问题来了:
虽然页面的注册数有限,但是一直长期保存历史记录,必然损耗性能。
我们需要设置一个消息的过期时间。
代码如下:
/* eslint-disable */
(function(global, factory) {
if (typeof module !== 'undefined') {
// nodeJs端
module.exports = factory();
} else if (typeof define === 'function') {
if (define.amd) {
// amd规范
define(factory);
} else if (define.cmd) {
// cmd规范
define(function(require, exports, module) {
module.exports = factory();
});
}
} else {
global.Observer = factory();
}
})(this, function () { 'use strict';
// 消息实体类
function Msg(type, args) {
this.type = type || 'default';
this.args = args || {};
this.timeId = new Date().getTime() + '_' + Msg.ID++;
this.getTime = function() {
return this.timeId.split('_')[0];
}
}
Msg.ID = 0;
function Observer() {
var _registTable = {}; // 注册表
var _msgQueue = []; // 消息队列
var _consumeHistory = {}; // 消费历史
// 增加消费记录
function addConsumeHistory(fnID, msgID) {
if (typeof _consumeHistory[msgID] === 'undefined') {
_consumeHistory[msgID] = [fnID];
} else {
_consumeHistory[msgID].push(fnID);
};
};
// 查询消费记录
function getConsumeHistory(fnID, msgID) {
var isConsume = false;
if (typeof _consumeHistory[msgID] === 'undefined') return isConsume;
var historyList = _consumeHistory[msgID];
if (historyList.indexOf(fnID) > -1) {
isConsume = true;
}
return isConsume;
};
// 清理过期消息 过期时间为五分钟
function removeMsg() {
var now = new Date().getTime();
for (var i = 0, len = _msgQueue.length; i < len; i++) {
if (now - _msgQueue[i].getTime() > 1000 * 60 * 5) {
_msgQueue.splice(i, 1);
}
}
}
//消费机制
function consume() {
removeMsg();
for (var i = 0; i < _msgQueue.length; i++) {
var msg = _msgQueue[i];
var registList = _registTable[msg.type];
if (registList instanceof Array) {
for (var j = 0; j < registList.length; j++) {
if (!getConsumeHistory(registList[j].registID, msg.timeId)) {
registList[j].call(this, msg);
addConsumeHistory(registList[j].registID, msg.timeId);
}
}
}
}
}
function regist(type, fn) {
if (typeof _registTable[type] === 'undefined') {
_registTable[type] = [fn];
} else {
_registTable[type].push(fn);
};
fn.registID = regist.ID++;
}
regist.ID = 0;
// 注册表
this.regist = function(type, fn) {
regist(type, fn);
consume();
};
// 发布消息
this.fire = function(type, args) {
_msgQueue.push(new Msg(type, args));
consume();
};
// 注销
this.remove = function(type, fn) {
if (_registTable[type] instanceof Array) {
for (var i = _registTable[type].length - 1; i >= 0; i--) {
if (_registTable[type][i] === fn) {
_registTable[type].splice(i, 1);
};
};
};
};
};
return new Observer();
});
10 看似完美
到现在为止,看似完美的解决了一切问题。实际上引入了很多概念。也增加了性能的负担,但js本身存在的环境并不会有很多消息存在。这些是足够用了。
要完全解这些问题,我们需要一个统一的东西来兼容这一切问题。
我个人觉得已经超过了观察者这个范围了。等我下次详细讲述。