资讯专栏INFORMATION COLUMN

从观察者模式到手写EventEmitter源码

cocopeak / 1177人阅读

摘要:观察者模式观察者模式广泛的应用于语言中,浏览器事件如鼠标单击,键盘事件都是该模式的例子。可以看到,这就是观察者模式的订阅方法实现。小结通过创建可观察的对象,当发生一个感兴趣的事件时可将该事件通告给所有观察者,从而形成松散的耦合。

观察者模式

观察者模式(observer)广泛的应用于javascript语言中,浏览器事件(如鼠标单击click,键盘事件keyDown)都是该模式的例子。设计这种模式背后的主要原因是促进形成低耦合,在这种模式中不是简单的对象调用对象,而是一个对象“订阅”另一个对象的某个活动,当对象的活动状态发生了改变,就去通知订阅者,而订阅者也称为观察者。

报纸订阅

生活中就像是去报社订报纸,你喜欢读什么报就去报社去交钱订阅,当发布了新报纸的时候,报社会向所有订阅了报纸的每一个人发送一份,订阅者就可以接收到。

我们可以利用这个例子来使用javascript来模拟一下。假设有一个发布者Jack,它每天出版报纸杂志,订阅者Tom将被通知任何时候发生的新闻。

Jack要有一个subscribers属性,它是一个数组类型,订阅的行为将会按顺序存放在这个数组中,而通知意味着调用订阅者对象的某个方法。因此,当用户Tom订阅信息的时候,该订阅者要向Jack的subscribe()提供他的一个方法。当然也可以退订,我不想再看报纸了,就调用unsubscribe()取消订阅。

一个简单的观察者模式应有以下成员:

subscribes 一个数组

subscribe() 将订阅添加到数组里

unsubscribe() 把订阅从数组中移除

publish() 迭代数组,调用订阅时的方法

这个模式中还需要一个type参数,用于区分订阅的类型,如有的人订阅的是娱乐新闻,有的人订阅的是体育杂志,使用此属性来标记。

我们使用简单的代码来实现它:

var Jack = {
    subscribers: {
        "any": []
    },
    //添加订阅
    subscribe: function (type = "any", fn) {
        if (!this.subscribers[type]) {
            this.subscribers[type] = [];
        }
        this.subscribers[type].push(fn); //将订阅方法保存在数组里
    },
    //退订
    unsubscribe: function (type = "any", fn) {
        this.subscribers[type] =
            this.subscribers[type].filter(function (item) { 
                return item !== fn;
            }); //将退订的方法从数组中移除
    },
    //发布订阅
    publish: function (type = "any", ...args) {
        this.subscribers[type].forEach(function (item) { 
            item(...args);    //根据不同的类型调用相应的方法
        });
    }
};

以上就是一个最简单的观察者模式的实现,可以看到代码非常的简单,核心原理就是将订阅的方法按分类存在一个数组中,当发布时取出执行即可。

下面使用Tom来订报:

var Tom = {
    readNews: function (info) {
        console.log(info);
    }
};

//Tom订阅Jack的报纸
Jack.subscribe("娱乐", Tom.readNews);
Jack.subscribe("体育", Tom.readNews);

//Tom 退订娱乐新闻:
Jack.unsubscribe("娱乐", Tom.readNews);

//发布新报纸:
Jack.publish("娱乐", "S.H.E演唱会惊喜登台")
Jack.publish("体育", "欧国联-意大利0-1客负葡萄牙");

运行结果:

欧国联-意大利0-1客负葡萄牙
观察者模式的实际应用

可以看到观察者模式将两个对象的关系变得十分松散,当不需要订阅关系的时候删掉订阅的语句即可。那么在实际应用中有哪些地方使用了这个模式呢?

events模块

node.js的events是一个使用率很高的模块,其它原生node.js模块都是基于它来完成的,比如流、HTTP等,我们可以手写一版events的核心代码,看看观察者模式的实际应用。

events模块的功能就是一个事件绑定,所有继承自它的实例都具备事件处理的能力。首先它是一个类,我们写出它的基本结构:

function EventEmitter() {
    //私有属性,保存订阅方法
    this._events = {};
}

//默认最大监听数
EventEmitter.defaultMaxListeners = 10;

module.exports = EventEmitter;

下面我们一个个将events的核心方法实现。

on方法

首先是on方法,该方法用于订阅事件,在旧版本的node.js中是addListener方法,它们是同一个函数:

EventEmitter.prototype.on =
    EventEmitter.prototype.addListener = function (type, listener, flag) {
        //保证存在实例属性
        if (!this._events) this._events = Object.create(null);

        if (this._events[type]) {
            if (flag) {//从头部插入
                this._events[type].unshift(listener);
            } else {
                this._events[type].push(listener);
            }

        } else {
            this._events[type] = [listener];
        }
        //绑定事件,触发newListener
        if (type !== "newListener") {
            this.emit("newListener", type);
        }
    };

因为有其它子类需要继承自EventEmitter,因此要判断子类是否存在_event属性,这样做是为了保证子类必须存在此实例属性。而flag标记是一个订阅方法的插入标识,如果为"true"就视为插入在数组的头部。可以看到,这就是观察者模式的订阅方法实现。

emit方法
EventEmitter.prototype.emit = function (type, ...args) {
    if (this._events[type]) {
        this._events[type].forEach(fn => fn.call(this, ...args));
    }
};

emit方法就是将订阅方法取出执行,使用call方法来修正this的指向,使其指向子类的实例。

once方法
EventEmitter.prototype.once = function (type, listener) {
    let _this = this;

    //中间函数,在调用完之后立即删除订阅
    function only() {
        listener();
        _this.removeListener(type, only);
    }
    //origin保存原回调的引用,用于remove时的判断
    only.origin = listener;
    this.on(type, only);
};

once方法非常有趣,它的功能是将事件订阅“一次”,当这个事件触发过就不会再次触发了。其原理是将订阅的方法再包裹一层函数,在执行后将此函数移除即可。

off方法
EventEmitter.prototype.off =
    EventEmitter.prototype.removeListener = function (type, listener) {

        if (this._events[type]) {
        //过滤掉退订的方法,从数组中移除
            this._events[type] =
                this._events[type].filter(fn => {
                    return fn !== listener && fn.origin !== listener
                });
        }
    };

off方法即为退订,原理同观察者模式一样,将订阅方法从数组中移除即可。

prependListener方法
EventEmitter.prototype.prependListener = function (type, listener) {
    this.on(type, listener, true);
};

此方法不必多说了,调用on方法将标记传为true(插入订阅方法在头部)即可。

以上,就将EventEmitter类的核心方法实现了。

小结

通过创建“可观察的”对象,当发生一个感兴趣的事件时可将该事件通告给所有观察者,从而形成松散的耦合。

部分实例参考《JavaScript模式》作者:Stoyan Stefanov

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/97694.html

相关文章

  • 解析nodeJS模块源码 亲手打造基于ES6的察者系统

    摘要:为指定事件注册一个单次监听器,即监听器最多只会触发一次,触发后立刻解除该监听器。移除指定事件的某个监听器,监听器必须是该事件已经注册过的监听器。返回指定事件的监听器数组。如何创建空对象我们已经了解到,是要来储存监听事件监听器数组的。 毫无疑问,nodeJS改变了整个前端开发生态。本文通过分析nodeJS当中events模块源码,由浅入深,动手实现了属于自己的ES6事件观察者系统。千万不...

    csRyan 评论0 收藏0
  • JavaScript 发布-订阅模式

    摘要:发布订阅模式订阅者把自己想订阅的事件注册到调度中心,当发布者发布该事件到调度中心,也就是该事件触发时,由调度中心统一调度订阅者注册到调度中心的处理代码。 发布-订阅模式,看似陌生,其实不然。工作中经常会用到,例如 Node.js EventEmitter 中的 on 和 emit 方法;Vue 中的 $on 和 $emit 方法。他们都使用了发布-订阅模式,让开发变得更加高效方便。 一...

    13651657101 评论0 收藏0
  • Node 之 Event 模块

    摘要:为什么把叫做集合而不能称为严格意义上的对象,来看这个集合的构造函数可以见得,是与处于同一层级的而非是继承自,所以说由实例出来的对象更加的纯净,并没有诸如等方法,更像是一个集合。 写在前面 事件的编程方式具有轻量级、松耦合、只关注事务点等优势,在浏览器端,有着自己的一套DOM事件机制,其中含包括这诸如事件冒泡,事件捕获等;然而Node的事件机制没有事件冒泡等,其原理就是设计模式中的观察者...

    mrli2016 评论0 收藏0
  • 循序渐进教你实现一个完整的node的EventEmitter模块

    摘要:本文从的的使用出发,循序渐进的实现一个完整的模块。移除指定事件的某个监听器,监听器必须是该事件已经注册过的监听器的别名移除所有事件的所有监听器,如果指定事件,则移除指定事件的所有监听器。返回指定事件的监听器数组。 node的事件模块只包含了一个类:EventEmitter。这个类在node的内置模块和第三方模块中大量使用。EventEmitter本质上是一个观察者模式的实现,这种模式可...

    sunsmell 评论0 收藏0
  • EventEmitter命令式 JavaScript class 声明函数式的华丽转身

    摘要:典型和改造挑战了解事件发布订阅系统实现思想,我们来看一段简单且典型的基础实现上面代码,实现了一个类我们维护一个类型的,对不同事件的所有回调函数进行维护。方法对指定事件进行回调函数存储方法对指定的触发事件,逐个执行其回调函数。 showImg(https://segmentfault.com/img/remote/1460000014287200); 新书终于截稿,今天稍有空闲,为大家奉...

    hsluoyz 评论0 收藏0

发表评论

0条评论

cocopeak

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<