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

152 阅读3分钟

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

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

什么是观察者模式

定义一对多的依赖关系,当一个对象发生改变,所有依赖她的对象都得到通知。
优势 1.可应用异步编程,替代回调。2.解耦,不必显示调用另一对象某个接口
弊端 模块之间的联系被隐藏了。

观察者模式有一个别名是发布-订阅模式,但经过发展发布订阅模式已经独立于观察者模式形成新的设计模式,两者的区别在于发布订阅模式具备调度中心

image.png

mitt源码分析

使用方式如下:

import mitt from 'mitt'

const emitter = mitt()

// listen to an event
emitter.on('foo', e => console.log('foo', e) )

// listen to all events
emitter.on('*', (type, e) => console.log(type, e) )

// fire an event
emitter.emit('foo', { a: 'b' })

// clearing all events
emitter.all.clear()

// working with handler references:
function onFoo() {}
emitter.on('foo', onFoo)   // listen
emitter.off('foo', onFoo)  // unlisten

源码比较简单,核心代码如下:

export default function mitt(all){
	all = all || new Map();
	return {
		all,
		on(type, handler) {
			const handlers = all.get(type);
			if (handlers) {
				handlers.push(handler);
			}
			else {
				all.set(type, [handler]);
			}
		},
		off(type, handler){
			const handlers = all.get(type);
			if (handlers) {
				if (handler){
					handlers.splice(handlers.indexOf(handler)>>>0, 1);
				}
				else {
					all.set(type, []);
				}
			}
			
		},
		emit(type, evt){
			let handlers = all.get(type);
			if (handlers) {
				handlers 
					.slice()
					.map((handler) => {
						handler(evt);
					});
			}
			handlers = all.get('*');
			if (handlers) {
				handlers
					.slice()
					.map((handler) => {
						handler(type, evt);
					});
			}

		}
	};
}

代码解读亮点

  1. 采用Map进行关系保存,而不是之前的Object。
  2. 在off的Api中使用了无符号右移。在以往的代码中我们可能会找取对应索引,判断是否存在,存在才移除。但使用无符号右移,若不存在的话则会得到一个极大正数,那么使用splice也并不会删除对应的监听。 image.png
  3. 监听所有事件的监听器,在on的时候type为*,则监听所有事件。emit的时候除了触发自身当前type的事件,也会触发所有事件的。

tiny-emitter源码分析

源码比较简单,如下:

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) {
    var e = this.e || (this.e = {});

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

    return this;
  },

  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);
  },

  emit: function (name) {
    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++) {
      evtArr[i].fn.apply(evtArr[i].ctx, data);
    }

    return this;
  },

  off: function (name, callback) {
    var e = this.e || (this.e = {});
    var evts = e[name];
    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;
  }
};

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

代码解读亮点

  1. 相比mitt多了once方法,解决思路是重新创建回调命名为listen,在执行真正的cb前调用off,并将原函数放入listen的_属性中。同时在off中检查这个_属性。
  2. 主体方法on、off、emit函数最后都返回了this,实现链式调用
  3. 一些判断赋值简写,例如(e[name] || (e[name] = [])).push

以 Class 方式重写 tiny-emitter

class EventEmitter{
    // 用来存放注册的事件与回调
    constructor(){
        this._events = {};
    }
    on(eventName, callback){
        // 由于一个事件可能注册多个回调函数,所以使用数组来存储事件队列
        this._events[eventName] = [...(this._events[eventName] || []), callback];
    }
    emit(eventName, ...args){
        const callbacks = this._events[eventName] || [];
        callbacks.forEach(callback => callback(...args));
    }
    off(eventName, callback){
        if(callback){
            this._events[eventName] = this._events[eventName].filter(fn => fn != callback && fn.initialCallback != callback)
        }else{
            this._events[eventName] = []
        }
    }
    once(eventName, callback){
        // 由于需要在回调函数执行后,取消订阅当前事件,所以需要对传入的回调函数做一层包装,然后绑定包装后的函数
        const one = (...args)=>{
            // 执行回调函数
            callback(...args)
            // 取消订阅当前事件
            this.off(eventName, one)
        }
        // 由于:我们订阅事件的时候,修改了原回调函数的引用,所以,用户触发 off 的时候不能找到对应的回调函数
        // 所以,我们需要在当前函数与用户传入的回调函数做一个绑定,我们通过自定义属性来实现
        one.initialCallback = callback;
        this.on(eventName, one)
    }
}

总结与收获

经过这两个库的通读,对观察者模式理解更深刻了。同时MITT是TS编写的,通读过程掌握TS一些关键字的使用。