【若川视野 x 源码共读】第8期 | 从 mitt、tiny-emitter 看观察者模式

137 阅读8分钟

本文参加了由公众号@若川视野 发起的每周源码共读活动,点击了解详情一起参与。

本期共读的两个模块mitt和tiny-emitter是对观察者模式的实现。

什么是观察者模式?

👇下面是《Head First设计模式》对观察者模式定义:

观察者模式定义了对象之间的一对多依赖,这样一来,当一个对象改变状态时,它的所有依赖者都会收到通知并自动更新。

image.png

观察者模式的优点

观察者和被观察者之间以松耦合(loosecouple)的方式结合。被观察者只需要以事件的方式发送通知,而不需要知道观察者的细节。

下面结合mitt和tiny-emitter的源码,看一下js中观察者模式的实现。

1. mitt

1.1 调试方法

可以参考【VSCode】使用ts-node 调试TypeScript代码

1.2 官网模块简介

Tiny 200b functional event emitter / pubsub. 特点:

  • 体积小,200b
  • 简单,基于单个函数的事件发射器或发布订阅。

1.3 用法举例

import mitt from 'mitt';
const emitter = mitt();
const fooHandler = () => {
	console.log('foo');
};
const allHandler = () => {
	console.log('all');
};
// 注册foo事件
emitter.on('foo',fooHandler);
// 添加*事件,即对所有
emitter.on('*', allHandler);
emitter.emit('foo'); // 'foo' 'all'
// 移除foo事件
emitter.off('foo');
emitter.emit('foo'); // 'all'

1.4 源码解析

源码整体由一个函数构成。

export default function mitt(all) {
	
	all = all || new Map();

	return {

		// 一个存储事件和对应的handler的Map
		all,

		// 注册事件及handler的方法
		on(type, handler) {
                // ...
		},

		// 移除事件或handler的方法
		off(type, handler) {
                // ...
		},

		// 触发事件的方法
		emit(type, evt) {
                // ...
		}
	};
}

调用mitt函数可以得到一个对象,该对象包含1个Map和3个函数方法。

1.4.1 all

1)Map类型,用于存储注册的事件event(Map的key)和事件对应的handlers(Map的value),同一个event的多个handler以数组形式存储。

2)all可以在调用mitt函数时以参数形式传入,否则会调用new Map()生成一个空Map实例。

function mitt(all) {
	all = all || new Map();
	// ...
}
1.4.2 on方法

该方法用于注册事件及handler

/**
 * Register an event handler for the given type.
 * @param {string|symbol} type Type of event to listen for, or `'*'` for all events
 * @param {Function} handler Function to call in response to given event
 * @memberOf mitt
 */
on<Key extends keyof Events>(type: Key, handler: GenericEventHandler) {
	const handlers: Array<GenericEventHandler> | undefined = all!.get(type);
	if (handlers) {
		handlers.push(handler);
	}
	else {
		all!.set(type, [handler] as EventHandlerList<Events[keyof Events]>);
	}
}

同一事件type的不同handler以数组形式存储。如果是已经注册过的事件,找到该事件对应的handlers数组。向handlers数组push新添加的handler。如果是没有注册过的事件,以事件type为key,[handler]为value,向map中添加新的键值对。

1.4.3 off方法

该方法用于移除事件及handler

/**
* Remove an event handler for the given type.
* If `handler` is omitted, all handlers of the given type are removed.
* @param {string|symbol} type Type of event to unregister `handler` from (`'*'` to remove a wildcard handler)
* @param {Function} [handler] Handler function to remove
* @memberOf mitt
*/
off<Key extends keyof Events>(type: Key, handler ?: GenericEventHandler) {
	const handlers: Array<GenericEventHandler> | undefined = all!.get(type);
	if (handlers) {
		if (handler) {
			handlers.splice(handlers.indexOf(handler) >>> 0, 1);
		}
		else {
			all!.set(type, []);
		}
	}
}

移除事件分成两种情况:

1)同时传入type和handler。这种情况代表删除事件type中的指定handler。这里用数组的splice方法零填充右移位运算>>>删除数组中指定的handler。如果找到了对应的handler,即handlers.indexOf(handler)大于等于0,handlers.indexOf(handler) >>> 0的值为index本身,因此可以将handler找到后删除。如果没有找到handlers.indexOf(handler)值为-1,则-1 >>> 0值为4294967295,远超正常注册handler的数量。而splice方法的第一个参数如果超出数组的长度,代表向数组末尾插入元素,但是这里没有传入元素,因此数组不变。

2)只传type。这种情况代表删除事件type对应的所有handlers。只要将该事件对应的handlers置空即可。这里all后面的!是typescript中的非空断言符号,代表all这个值在逻辑上不为空值null或undefined。

注意:需要移除的handler必须跟注册时的handler是相同的引用。

1.4.4 emit方法

该方法用于触发事件

/**
 * Invoke all handlers for the given type.
 * If present, `'*'` handlers are invoked after type-matched handlers.
 *
 * Note: Manually firing '*' handlers is not supported.
 *
 * @param {string|symbol} type The event type to invoke
 * @param {Any} [evt] Any value (object is recommended and powerful), passed to each handler
 * @memberOf mitt
 */
emit<Key extends keyof Events>(type: Key, evt ?: Events[Key]) {
	let handlers = all!.get(type);
	if (handlers) {
		(handlers as EventHandlerList<Events[keyof Events]>)
			.slice()
			.map((handler) => {
				handler(evt!);
			});
	}

	handlers = all!.get('*');
	if (handlers) {
		(handlers as WildCardEventHandlerList<Events>)
			.slice()
			.map((handler) => {
				handler(type, evt!);
			});
	}
}

emit逻辑分为3种情况:

1)如果事件type注册了对应的handlers,则遍历并执行handlers,并且可以通过第二个参数evt传参数给handler.

2)如果注册了事件*,则遍历并执行事件*的handlers。

3)如果事件type未注册,则什么也不执行。

2. tiny-emitter

2.1 调试方法

参考【VSCode】调试nodejs代码

2.2 官网模块简介

A tiny (less than 1k) event emitter library. 特点:

  • 体积小,<1k
  • 简单,基于构造函数和原型的事件发射器。

2.3 用法举例

const Emitter = require('./index');
const emitter = new Emitter();
const context = {
    contextValue: 'context'
};

function contextHandler() {
    console.log(this.contextValue);
}

function onceHandler() {
    console.log('once');
}

function onHandler() {
    console.log('on');
}
// 注册事件test及handler,并传入上下文context
emitter.on('test', contextHandler, context);
emitter.once('test', onceHandler);
emitter.on('test', onHandler);
// 触发事件test
emitter.emit('test');
// 'context'
// 'once' 仅被触发一次
// 'on'
// 移除test事件中的 contextHandler
emitter.off('test', contextHandler);
// 触发事件test
emitter.emit('test');
// 'on'

2.4 源码解析

源码整体由一个空函数E和函数E的原型方法构成。

function E() {
    // Keep this empty so it's easier to inherit from
    // (via https://github.com/lipsmack from https://github.com/scottcorgan/tiny-emitter/issues/3)
}

E.prototype = {

    on: function (name, callback, ctx) {
        // 注册事件
    },
    once: function (name, callback, ctx) {
        // 注册单次触发事件
    },
    emit: function (name) {
        // 触发事件
    },
    off: function (name, callback) {
        // 注销事件handler
    }
};

module.exports = E;
module.exports.TinyEmitter = E;

函数E的原型上有on、emit、off、once四个方法。

2.4.1 on方法

该方法用于注册事件及handler,可绑定上下文ctx

  on: function (name, callback, ctx) {
  // 用实例上的e对象存放注册的事件
    var e = this.e || (this.e = {});

    (e[name] || (e[name] = [])).push({
      fn: callback,
      ctx: ctx
    });

    return this;
  }

tiny-emitter用实例上的e对象存放注册的事件。当注册新事件handler时,会将事件做key,handler和ctx做value,存入e对象的map中。对于同名事件,支持注册多个handler及对应的上下文,多个handler以数组的方式存储。

2.4.2 emit方法

该方法用于触发事件对应的handlers

  emit: function (name) {
    // 第一个参数之外的其他参数会作为handler的入参data
    var data = [].slice.call(arguments, 1);
    var evtArr = ((this.e || (this.e = {}))[name] || []).slice();
    var i = 0;
    var len = evtArr.length;

    for (i; i < len; i++) {
      // 绑定注册时传入的上下文ctx,并传入参数data
      evtArr[i].fn.apply(evtArr[i].ctx, data);
    }

    return this;
  }

emit支持多个入参,第一个参数name作为待出发的时间名称,其他参数作为handler的入参data。 emit方法会遍历事件name对应的每个handler,绑定注册时传入的ctx上下文并传入参数data.

2.4.3 off方法

该方法用于移除指定的事件handler

  off: function (name, callback) {
    var e = this.e || (this.e = {});
    var evts = e[name];
    // 用于存储未被移除的handler,默认移除所有的handler,即默认为[].
    var liveEvents = [];

    if (evts && callback) {
      for (var i = 0, len = evts.length; i < len; i++) {
        if (evts[i].fn !== callback && evts[i].fn._ !== callback)
          liveEvents.push(evts[i]);
      }
    }

    // Remove event from queue to prevent memory leak
    // Suggested by https://github.com/lazd
    // Ref: https://github.com/scottcorgan/tiny-emitter/commit/c6ebfaa9bc973b33d110a84a307742b7cf94c953#commitcomment-5024910

    (liveEvents.length)
      ? e[name] = liveEvents
      : delete e[name];

    return this;
  }

该方法用数组liveEvents暂存未被移除的handler。

1)如果只传入事件名称name。则liveEvents.length === 0,执行delete e[name]移除事件name对应的所有handler。

2)如果传入事件名称name和指定的handler即callback。则将指定handler以外的其他handler存入liveEvents,最后将liveEvents作为更新后的handlers。

注意:需要移除的handler必须跟注册时的handler是相同的引用。

2.4.4 once方法

该方法用于注册单次触发的事件handler

  once: function (name, callback, ctx) {
    var self = this;
    function listener () {
      self.off(name, listener);
      callback.apply(ctx, arguments);
    };

    listener._ = callback
    return this.on(name, listener, ctx);
  }

该方法将传入的handler(即callback)包装成新的handler函数listener。调用on方法注册事件及listener。listener被触发后会调用on方法从事件name中移除listener。 这里需要注意,实际注册的handler是包装后的listener,而不是传入的callback

如果想在事件触发前移除注册的事件怎么办呢?这里用listener._ = callback将原来的handler挂在listener的_属性上。off方法中保留符合条件handler的逻辑如下:

if (evts[i].fn !== callback && evts[i].fn._ !== callback)
          liveEvents.push(evts[i]);
      }

通过once注册的handler会因为满足evts[i].fn._ === callback 而被移除。

mitt VS tiny-emitter

对比项mitttiny-emitter
实现原理基于函数,调用函数生成的每个emitter中各有一套map和on、off、emit方法。基于构造函数和原型,不同的emitter实例共享同一套on、once、off、emit方法。
*事件,即触发任何事件都会出发*事件相应的handlers。支持不支持
once不支持支持
在on方法中指定上下文context不支持支持
存储事件的数据结构Map普通js对象{}

总结

1) 通过mitt和tiny-emitter学习了观察者模式的js实现。

2) 二者都用map结构存储事件及其handler.

4) 二者都实现了on、off、emit方法用于事件handler的注册、移除和触发。

3) 观察者模式将被观察者和观察者以松耦合方式结合,有利于被观察者和观察者的逻辑分离。

参考文档

  1. 观察者模式(维基百科)
  2. 发布/订阅(维基百科)
  3. 《Head First设计模式》