因为在一个项目中,需要在多个page建立所谓的websocket。但是这存在一个很不实际的问题,如果在每个页面建立websocket,第一不利于页面维护,第二多次建立websocket对内存和项目不定性不利,所以想到使用一种叫《事件总线》实例对所有的通讯事件进行调度和维护。
这里先介绍下《事件总线》的原理:
在前端的世界观里,每个页面的javascript在不被引用的情况下是无法被调用的,这是因为每个javascript内的function是绑定在window下的。而默认下window是可以被忽略不写的。
window.jquery.fn() = jquery.fn()
因而在正常的网页下,javascript的方法仅仅存在与当前页面(window这个全局变量)下的。因为在网页中是很难使用类似《事件总线》的思路,当然浏览器有postMessage的方法,通讯更为便利。(有同学肯定要跟我抬杠,网页也可以。我说的是很难凸^-^凸)
而在小程序下,没有window,所以事件都不会依赖于window,而除了page自带的所谓的javascript,其余引入的javascript文件都必须使用ES6的语法,对外暴露调用的接口来书写的,在引用页面通过require来引用,进而为事件总线的思路提供得天独厚的方便。
使用《事件总线》的理由和条件
对于一些需要长期进行tcp连接的,如websocket、需要进行长期运行的函数方法,甚至是有着复杂前置方法的同步闭包方法,才需要用到《事件总线》。
1、长期运行的函数方法:
const timerIndex = setInterval(() => { //一个需要长期运行的函数方法,且在很多个页面都有一样或者不一样的回调 }, 1000)
2、有着复杂前置方法的同步闭包方法
很拗口是吧~因为js是一个原型链,导致在很多地方会出现同步的且需要回调的情况
//当成是一个很复杂的整理数据结构的代码 //说什么封包什么的,也解决不了代码复杂的问题 //当然你可以说用promise.js,去promise一个http请求,可是一样会出现难以维护的问题,如果如果接口更换。 //http请求 success:function(data){ //成功的回调函数 }, fail:function(){ //失败的回调函数 }
使用《事件总线》的思路
要做到可以事件总线其实不难,我们分为3个部分了解下
1、接受事件
接受指定事件name后需要执行一个回调方法callback,而且为了区分这个callback是归属于某个页面的方便移除,我们则必须而外给定一个参数self。而用于存储多个事件的容器我们命名为events。
let events = {};
接受事件的伪代码
const on = function (name, self, callback) { const tuple = [self, callback]; events[name] = tuple; //把self和callback作为事件的属性,放入以name为下标的数组中,最后放入容器events用于存储 //思考,假设我在其他页面中需要接受同样name的事件,但是回调方法是不一样的那如何处理 }
2、移除事件
events这个容器会随着放入事件的增加而无休止的增加,并且会占用内存,释放内存机制必不可少的,所以在小程序页面被卸载的时候必须移除该页面对应的接受事件的回调。
移出事件的伪代码
const remove = function (name, self) { const callbacks = events[name]; events[name] = []; //思考,如果同一个name,有对应多个回调事件;而我仅仅仅仅仅仅只需要移出当前页面上这个对应的回调事件怎么办 }
3、执行回调
通过接受到的name和data可以取出对应events下对应name的所有回调数组,执行保存在内存中的回调函数即可
执行回调的伪代码
const emit = function (name, data) { const callbacks = events[name]; const self = tuple[0]; const callback = tuple[1]; callback.call(self, data); //思考如果不存在这个name,那么肯定会报错,要怎么处理呢 }
优化代码
一个插件或者一套系统要可以复用才是最终目的,在复用过程中很多时候会考虑一些极端错误的情况,比如不存在,或者有很多这样,于是我们必须对代码进行修改容错
正如上一节在思考中提到3个问题:
1、events[name]并不仅仅可以存放一个回调事件,我们要让它可以存放多个。于是自然而然的想到了数值,而在压入数组我们要先判断原始的event[name]是不是一个数组,否则push(Array)会报错。
接受事件的完整代码
const on = function (name, self, callback) { const tuple = [self, callback]; const callbacks = events[name]; if (Array.isArray(callbacks)) { callbacks.push(tuple); } else { events[name] = [tuple]; } }
2、仅仅想移出对应事件的回调函数,那么我们仅仅需要移除events[name]这个数组对应self与传入的self相同的回调就可以了
移除事件的完整代码
const remove = function (name, self) { const callbacks = events[name]; if (Array.isArray(callbacks)) { events[name] = callbacks.filter((tuple) => { return tuple[0] != self; }) } }
3、因为一个name可能有多个回调,那么如果要全部执行的话,其实就是一个遍历数组的过程,然后调用数组下标为[1]的方法,也就是callback
执行的完整代码
const emit = function (name, data) { const callbacks = events[name]; if (Array.isArray(callbacks)) { callbacks.map((tuple) => { const self = tuple[0]; const callback = tuple[1]; callback.call(self, data); }) } }
《事件总线》系统示意图
《事件总线》完整代码
最后根据小程序的要求,使用 module.export 暴露调用的接口
完整代码如下
let events = {}; const on = function (name, self, callback) { const tuple = [self, callback]; const callbacks = events[name]; if (Array.isArray(callbacks)) { callbacks.push(tuple); } else { events[name] = [tuple]; } } const remove = function (name, self) { const callbacks = events[name]; if (Array.isArray(callbacks)) { events[name] = callbacks.filter((tuple) => { return tuple[0] != self; }) } } const emit = function (name, data) { const callbacks = events[name]; if (Array.isArray(callbacks)) { callbacks.map((tuple) => { const self = tuple[0]; const callback = tuple[1]; callback.call(self, data); }) } } module.exports = { on: on, remove: remove, emit: emit }
源码已在gitee开源,使用es6规范编写,并已经投入生产中,请放心使用。