JavaScript从观察者模式到eventEmitter

观察者者模式

很早以前写了一篇关于观察者模式(发布订阅模式)的简单实现, 现在有了新的感悟, 捡起来再啃啃.

先上维基百科看看:

观察者模式是软件设计模式的一种。在此种模式中,一个目标对象管理所有相依于它的观察者对象,并且在它本身的状态改变时主动发出通知。这通常透过呼叫各观察者所提供的方法来实现。此种模式通常被用来实时事件处理系统。

理解起来很简单: 我去书报亭订了一份报纸,当他把报纸送给我了,我就去领了看.

这里,我就变成了订阅者,报亭就是发布者,当报纸送到的时候(状态发生改变,通知订阅者),我就去领了看(做一些操作).

我们看到, 订阅者模式有两个实体, 一个发布者, 一个是订阅者.

发布者有三个功能: 订阅, 发布, 退订.

简单到爆炸的版本

🤓让我们动手来写一个试试.

订阅功能:

let Publisher = {};
let eventQueue = {}; // 存事件, 一个事件名对应一个函数

publisher.subscribe = function(event, listener) {
     eventQueue[event] = listener;
}

发布功能:

publisher.publish = function(event) {
    let listener = eventQueue[event];
    listener && listener();             // 如果存在, 则调用监听器
}

退订功能:

// 将监听器从 eventQueue 删除(设为 null)
publisher.off = function(event, listener) {   
    if(!eventQueue[event]) return;
    eventQueue[event] = null;
}

这样, 我们就实现了一个简陋的观察者模式了

publisher.subscribe("eventA", function() {
    console.log("eventA, publish!");
});
publisher.publish("eventA");     // eventA, publish!"

但是这样也太简陋了, eventQueue 都是暴露在外的, 而且一个事件只能订阅一个操作而且没办法传参.

完善一下

让我们改进一下, 加入给事件监听器传参的功能, 一个事件注册多个监听器

let publisher = (function(){
    let eventQueue = {};  // 将事件存在闭包中, 每一个事件,保存一个监听器数组

      return {

        subscribe(event, listener) {
            if(eventQueue[event]) {
                eventQueue[event].push(listener);
            } else {
                eventQueue[event] = [];
                eventQueue[event].push(listener);
            }

              // 下面这个写法简单些, 但是不好读..
              // (eventQueue[event] = eventQueue[event] || []).push(listener);
        },


        publish(event, ...args) {
            let listeners = eventQueue[event];
            if(!listeners) return;
            listeners.forEach((listener) => {
                listener(...args);
            })
        },


        off(event, listener) {
            let listeners = eventQueue[event];
            eventQueue[event] = listeners.filter((l) => {
                return l !== listener;
            });
        }
    }
}());

// 测试
function listener(a, b) {
    console.log(`a: ${a}`);
    console.log(`b: ${b}`);
}

publisher.subscribe("eventA", listener);
publisher.publish("eventA", "hello", "world"); //a: hello
                                           //b: world
publisher.off("eventA", listener);
publisher.publish("eventA", "hello", "world"); // 没输出东西的

这样, 我们就实现了一个简单的观察者模式了. (但是这样一点也不 OO啊)

让我把它封装成一个类, 这样它就 OO了.

function Publisher() {
    this.event = {};
}



Publisher.prototype.subscribe = function(event, listener) {
    if(this.eventQueue[event]) {
        this.eventQueue[event].push(listener);
    } else {
        this.eventQueue[event] = [];
        this.eventQueue[event].push(listener);
    }
}



Publisher.prototype.publish = function(event, ...args) {
    let listeners = this.eventQueue[event];
    if(!listeners) return;
    listeners.forEach((listener) => {
        listener(...args);
    });
}



Publisher.prototype.off = function(event, listener) {
    let listeners = this.eventQueue[event];
    this.eventQueue[event] = listeners.filter((l) => {
        return l !== listener;
    });
}

用起来试试:

let publisher = new Publisher();

publisher.subscribe("eventA", listener);
publisher.publish("eventA", "hello", "world"); // a: hello b: world
publisher.off("eventA", listener);
publisher.publish("eventA", "hello", "world"); // 没输出

OK 辣, 功能也还算完善

然而, 如果是老司机, 一眼就能看出来, 其实这个东西是和 node 的 EventEmitter 很像的: 添加事件监听, 发布事件.

我们只需要改该名字, 就可以伪装成 node 的 EventEmitter 了

FBI warning: 下面代码其实和上面是一样的, 只是改了属性方法名而已;

function EventEmitter() {
    this._eventQueue = {};
}
EventEmitter.prototype.on = function(event, listener) {
    if(this._eventQueue[event]) {
        this._eventQueue[event].push(listener);
    } else {
        this._eventQueue[event] = [];
        this.eventQuone[event].push(listener);
    }
}

EventEmitter.prototype.emit = function(event, ...args) {
    let listeners = this._eventQueue[event];
    if(!listeners) return;
    listeners.forEach((listener) => {
        listener(...args);
    });
}

EventEmitter.prototype.off = function(event, listener) {
    let listeners = this._eventQueue[event];
    this._eventQueue[event] = listeners.filter((l) => {
        return l !== listener;
    });
}

let emitter = new Emitter();
emitter.on("request", function() {
    console.log("on request");
});
emitter.emit("request");
// do some thing

事件

然而, 我们写那么多有什么用?

EventEmitter 是 node 的核心模块, 回忆一下,

写 nodejs 代码的时候, 是不是经常写一些事件监听?

举个例子:

http.createServer(function(request, response) {
    request.on("data", function() {
        // do something
    });
})

其实, 看似简单的代码, 背后总是隐藏着后面无数代码的辛勤工作

request 是 http.IncomingMessage 类的一个实例, (每一个请求, 都会 new 一个http.IncomingMessage 然后传入到上面的匿名函数里面去);

http.IncomingMessage 类直接继承了 EventEmitter 类

以下请假装是伪代码, 简单模拟了一下

let http = require('http');
let events = require('event');


/***********************
    假装这里是http模块里的代码
***********************/
http.IncomingMessage = function() {
    events.EventEmitter.call(this);
    // something else
}
utils.inherits(http.IncomingMessage, events.EventEmitter);


/******************
createSever 
*******************/

http.createServer = function(listener) {
  // balabala
    let request = new http.IncomingMessage();
      listenter(request, response);   // response就不写了
  // do something
}


/*****************
    这里,你 create 了一个 http server
    request 其实是 IncomingMessage 的实例

****************/
http.createServer(function(request, response) {
    request.on("data", function(data) {
        // do something
    });
});


/***********
    你写完上面那里以后, eventEmitter 默默的把你上面的匿名函数放到事件队列里面了
**************/


// 看这里

/****
    然后用户发了一些数据, 比如请求

    然后运行 这段代码:  request.emit('data', data);
    然后你添加的事件监听函数就运行了
****/

//over

以上大致就是request 事件订阅和发布的过程了.

觉得太麻烦不想看?

我们来看看简单的用法:

如果你需要在代码中使用事件

你只需要:

function YourClass() {
    EventEmitter.call(this);
}
utils.inherts(YourClass, EventEmitter);

这样, YourClass 类的实例, 就可以用 on, emitter 去添加和发布事件了

然而, 我不用给自己的类添加事件也能写出功能来啊

这个事件类, 其实可以让你的代码减少嵌套举个例子, 以下代码不能运行

app.get(function(req, res) {
    let xxx = req.params['xxx'];
    mysql.query('select * from table where xxx=$', [xxx], function(result) {
        mysql.query('select * from table where xxx=${result}', function() {
            // do something
        });
    });
})



emitter.on("queryxxx", function(result) {
  mysql.query('select * from table where xxx=${result}', function() {
    // do something
  });
});

app.get(function(req, res) {
  let xxx = req.params['xxx'];
  mysql.query('select * from table where xxx=$', [xxx], function(result) {
    emitter.emit("queryxxx", result);
  })
})

可以看到事件可以减少代码嵌套过深的问题

( 当然其实用 promise, yeild也可以解决, 而且优雅的多.

然而其实它的作用当然不是这么简单的.

观察者模式更多的用于前端的 MV*框架

大概是这样: 数据改变->发布数据改变的事件-> 渲染页面

模拟一下:

data.on("change", function() {
    // 渲染页面, 和做一些其他的事情
});

// 向框架使用者提供改变 data 的接口 
data.setData = function(data) {
    // do something
      data.emit("change", data);
}

在 js 中,如果只是针对数据的改变触发事件的话,其实 getter, setter 是更好的选择, 很多框架就是使用了 getter, setter 实现的

-- Ming