您当前的位置:首页 > 计算机 > 编程开发 > Other

如何实现一个 Event Bus

时间:12-14来源:作者:点击数:

React/Vue 不同组件之间是怎么通信的?

Vue

  1. 父子组件用 Props 通信
  2. 非父子组件用 Event Bus 通信
  3. 如果项目够复杂,可能需要 Vuex 等全局状态管理库通信
  4. $dispatch(已经废除)和 $broadcast(已经废除)

React

  1. 父子组件,父->子直接用 Props,子->父用 callback 回调
  2. 非父子组件,用发布订阅模式的 Event 模块
  3. 项目复杂的话用 Redux、Mobx 等全局状态管理管库
  4. 用新的 Context Api

我们大体上都会有以上回答,接下来很可能会问到如何实现 Event(Bus),因为这个东西太重要了,几乎所有的模块通信都是基于类似的模式,包括安卓开发中的Event Bus,Node.js 中的 Event 模块(Node 中几乎所有的模块都依赖于 Event,包括不限于 http、stream、buffer、fs 等)

我们仿照 Node 中 Event API 实现一个简单的 Event 库,他是发布订阅模式的典型应用。

提前声明:我们没有对传入的参数进行及时判断而规避错误,仅仅对核心方法进行了实现。


1.基本构造

1.1 初始化 class

我们利用ES6的class关键字对Event进行初始化,包括Event的事件清单和监听者上限.

我们选择了Map作为储存事件的结构,因为作为键值对的储存方式Map比一般对象更加适合,我们操作起来也更加简洁,可以先看一下Map的基本用法与特点.

class EventEmeitter {  constructor() {    this._events = this._events || new Map(); // 储存事件/回调键值对    this._maxListeners = this._maxListeners || 10; // 设立监听上限  }}

1.2 监听与触发

触发监听函数我们可以用applycall两种方法,在少数参数时call的性能更好,多个参数时apply性能更好,当年Node的Event模块就在三个参数以下用call否则用apply.

当然当Node全面拥抱ES6+之后,相应的call/apply操作用Reflect新关键字重写了,但是我们不想写的那么复杂,就做了一个简化版.

// 触发名为type的事件EventEmeitter.prototype.emit = function(type, ...args) {  let handler;  // 从储存事件键值对的this._events中获取对应事件回调函数  handler = this._events.get(type);  if (args.length > 0) {    handler.apply(this, args);  } else {    handler.call(this);  }  return true;};
// 监听名为type的事件
EventEmeitter.prototype.addListener = function(type, fn) {
// 将type事件以及对应的fn函数放入this._events中储存
if (!this._events.get(type)) {
this._events.set(type, fn);
}
};

我们实现了触发事件的emit方法和监听事件的addListener方法,至此我们就可以进行简单的实践了.

// 实例化const emitter = new EventEmeitter();// 监听一个名为arson的事件对应一个回调函数emitter.addListener('arson', man => {  console.log(`expel ${man}`);});// 我们触发arson事件,发现回调成功执行emitter.emit('arson', 'low-end'); // expel low-end

似乎不错,我们实现了基本的触发/监听,但是如果有多个监听者呢?

// 重复监听同一个事件名emitter.addListener('arson', man => {  console.log(`expel ${man}`);});emitter.addListener('arson', man => {  console.log(`save ${man}`);});emitter.emit('arson', 'low-end'); // expel low-end

是的,只会触发第一个,因此我们需要进行改造。


2.升级改造

2.1 监听/触发器升级

我们的addListener实现方法还不够健全,在绑定第一个监听者之后,我们就无法对后续监听者进行绑定了,因此我们需要将后续监听者与第一个监听者函数放到一个数组里.

// 触发名为type的事件EventEmeitter.prototype.emit = function(type, ...args) {  let handler;  handler = this._events.get(type);  if (Array.isArray(handler)) {    // 如果是一个数组说明有多个监听者,需要依次此触发里面的函数    for (let i = 0; i < handler.length; i++) {      if (args.length > 0) {        handler[i].apply(this, args);      } else {        handler[i].call(this);      }    }  } else { // 单个函数的情况我们直接触发即可    if (args.length > 0) {      handler.apply(this, args);    } else {      handler.call(this);    }  }
return true;
};

// 监听名为type的事件
EventEmeitter.prototype.addListener = function(type, fn) {
const handler = this._events.get(type); // 获取对应事件名称的函数清单
if (!handler) {
this._events.set(type, fn);
} else if (handler && typeof handler === 'function') {
// 如果handler是函数说明只有一个监听者
this._events.set(type, [handler, fn]); // 多个监听者我们需要用数组储存
} else {
handler.push(fn); // 已经有多个监听者,那么直接往数组里push函数即可
}
};

是的,从此以后可以愉快的触发多个监听者的函数了。

// 监听同一个事件名emitter.addListener('arson', man => {  console.log(`expel ${man}`);});emitter.addListener('arson', man => {  console.log(`save ${man}`);});emitter.addListener('arson', man => {  console.log(`kill ${man}`);});// 触发事件emitter.emit('arson', 'low-end');//expel low-end//save low-end//kill low-end

2.2 移除监听

我们会用removeListener函数移除监听函数,但是匿名函数是无法移除的.

EventEmeitter.prototype.removeListener = function(type, fn) {  const handler = this._events.get(type); // 获取对应事件名称的函数清单
// 如果是函数,说明只被监听了一次
if (handler && typeof handler === 'function') {
this._events.delete(type, fn);
} else {
let postion;
// 如果handler是数组,说明被监听多次要找到对应的函数
for (let i = 0; i < handler.length; i++) {
if (handler[i] === fn) {
postion = i;
} else {
postion = -1;
}
}
// 如果找到匹配的函数,从数组中清除
if (postion !== -1) {
// 找到数组对应的位置,直接清除此回调
handler.splice(postion, 1);
// 如果清除后只有一个函数,那么取消数组,以函数形式保存
if (handler.length === 1) {
this._events.set(type, handler[0]);
}
} else {return this;}}};

3.发现问题

我们已经基本完成了 Event 最重要的几个方法,也完成了升级改造,可以说一个Event的骨架是被我们开发出来了,但是它仍然有不足和需要补充的地方。

  1. 鲁棒性不足: 我们没有对参数进行充分的判断,没有完善的报错机制。
  2. 模拟不够充分: 除了 removeAllListeners 这些方法没有实现以外,例如监听时间后会触发 newListener 事件,我们也没有实现,另外最开始的监听者上限我们也没有利用到

当然,这在面试中现场写一个 Event 已经是很够意思了,主要是体现出来对发布-订阅模式的理解,以及针对多个监听状况下的处理,不可能现场撸几百行写一个完整 Event。

索性 Event 库帮我们实现了完整的特性,整个代码量有300多行,很适合阅读,你可以花十分钟的时间通读一下,见识一下完整的 Event 实现:

(function (root, factory) {	if(typeof exports === 'object' && typeof module === 'object')		module.exports = factory();	else if(typeof define === 'function' && define.amd)		define("EventBus", [], factory);	else if(typeof exports === 'object')		exports["EventBus"] = factory();	else		root["EventBus"] = factory();})(this, function() {	var EventBusClass = {};	EventBusClass = function() {		this.listeners = {};	};	EventBusClass.prototype = {		addEventListener: function(type, callback, scope) {			var args = [];			var numOfArgs = arguments.length;			for(var i=0; i<numOfArgs; i++){				args.push(arguments[i]);			}			args = args.length > 3 ? args.splice(3, args.length-1) : [];			if(typeof this.listeners[type] != "undefined") {				this.listeners[type].push({scope:scope, callback:callback, args:args});			} else {				this.listeners[type] = [{scope:scope, callback:callback, args:args}];			}		},		removeEventListener: function(type, callback, scope) {			if(typeof this.listeners[type] != "undefined") {				var numOfCallbacks = this.listeners[type].length;				var newArray = [];				for(var i=0; i<numOfCallbacks; i++) {					var listener = this.listeners[type][i];					if(listener.scope == scope && listener.callback == callback) {					} else {						newArray.push(listener);					}				}				this.listeners[type] = newArray;			}		},		hasEventListener: function(type, callback, scope) {			if(typeof this.listeners[type] != "undefined") {				var numOfCallbacks = this.listeners[type].length;				if(callback === undefined && scope === undefined){					return numOfCallbacks > 0;				}				for(var i=0; i<numOfCallbacks; i++) {					var listener = this.listeners[type][i];					if((scope ? listener.scope == scope : true) && listener.callback == callback) {						return true;					}				}			}			return false;		},		dispatch: function(type, target) {			var event = {				type: type,				target: target			};			var args = [];			var numOfArgs = arguments.length;			for(var i=0; i<numOfArgs; i++){				args.push(arguments[i]);			};			args = args.length > 2 ? args.splice(2, args.length-1) : [];			args = [event].concat(args);			if(typeof this.listeners[type] != "undefined") {				var listeners = this.listeners[type].slice();				var numOfCallbacks = listeners.length;				for(var i=0; i<numOfCallbacks; i++) {					var listener = listeners[i];					if(listener && listener.callback) {						var concatArgs = args.concat(listener.args);						listener.callback.apply(listener.scope, concatArgs);					}				}			}		},		getEvents: function() {			var str = "";			for(var type in this.listeners) {				var numOfCallbacks = this.listeners[type].length;				for(var i=0; i<numOfCallbacks; i++) {					var listener = this.listeners[type][i];					str += listener.scope && listener.scope.className ? listener.scope.className : "anonymous";					str += " listen for '" + type + "'/n";				}			}			return str;		}	};	var EventBus = new EventBusClass();	return EventBus;});
方便获取更多学习、工作、生活信息请关注本站微信公众号城东书院 微信服务号城东书院 微信订阅号
推荐内容
相关内容
    无相关信息
栏目更新
栏目热门
本栏推荐