发布订阅模式以及 EventEmitter 类的实现

在前端开发中,经常会使用到发布订阅模式,发布订阅模式也被称为观察者模式。最常见的发布订阅模式莫过于给 DOM 绑定事件,当点击一个按钮或者鼠标移动到某个元素上就会触发事件监听函数,然后弹出一个文本框或者改变元素样式。

div.addEventListener("click", () => {
    alert("Hello!");
});

div 元素相当于一个订阅者,他会告诉浏览器(发布者),他要订阅一个 click 事件,这个事件会弹出一个提示框。当用户点击 div 元素时,浏览器就会“发布”这个消息,告知用户。

Node.js 中,也大量应用了发布订阅模式,它是构成 Node.js 平台的核心之一。比如在响应 post 请求时,想要拿到数据,可以给请求对象绑定 data 事件用来接收回传的数据,数据接收完毕后就会触发 end 事件,我们就可以在其中拿到完整的请求数据了。

const http = require("http");

const httpServer = http.createServer((req, res) => {
    let { url } = req;
    let method = req.method.toLocaleLowerCase();

    if(method === "post"){
        if(url === "/data"){
            let str = "";
            // 监听 data 事件,收集前端发来的数据块
            req.on('data', (chunk) => {
                str += chunk;
            });
            // 数据接收完成之后触发 end 事件
            req.on('end', () => {
                console.log("str === ", str);

                res.write("OK!");
                res.end();
            });
        }
    }
});
httpServer.listen(8888, () => {
    console.log("Server is running: http://localhost:8888");
});

前端利用发布订阅模式可以给元素绑定事件,Node.js 中利用发布订阅模式可以处理请求数据。这些发布订阅模式都是内置的,在 Node.js 中,有一个 events 模块,这个模块中有一个 EventEmitter 类,EventEmitter 可以将一个或多个函数注册为监听器,当事件触发时,相应的函数就会被调用。Node.js 中的许多模块都继承了这个类,拥有了事件监听的能力。

EventEmitter 内部维护着一个事件监听函数集,当内部的方法 emit 被调用后就会触发相应的监听函数。比如:

const EventEmitter = require("events").EventEmitter;

const event = new EventEmitter();

var c = () => console.log("aaa -- second");
// 订阅事件
event.on("aaa", () => console.log("aaa -- first"))
    .once("aaa", () => console.log("aaa -- once"))
    .on("aaa", c, true)

// 触发事件
event.emit("aaa");
console.log("\n");
event.emit("aaa");

// 移除某个事件函数
event.removeEventListener("aaa", c);
console.log("\n");
event.emit("aaa");

订阅事件可以链式调用,上面代码中注册了三个 aaa 事件,其中有一个 once 表明之触发一次。虽然事件函数名都叫 aaa,但因为绑定的函数是不同的,因此当调用 emit 是会触发多个函数执行。removeEventListener 可以移除 aaa 事件中的 c 函数。

要想让一个事件可以绑定多个监听函数,也很容易,只需把注册的函数存入数组中即可,当事件触发时把数组中的函数都执行一遍。下面就动手实现一个 EventEmitter 类。

EventEmitter

简单的实现一下。EventEmitter 类需要有一个存放监听函数的数据结构,用对象就好,对象的键是事件名称,值是数组用来存放监听函数。

class EventEmitter{
    event: {    // 存放 on 绑定的事件函数
        [eventName: string]: Function[];
    }
    onceEvent: {    // 存放 once 绑定的事件函数
        [eventName: string]: Function[];
    }

    constructor(){
        // 初始化
        this.event = {};
        this.onceEvent = {};
    }
}

on 和 once 函数

绑定事件监听函数,然后返回 EventEmitter 实例。
once 函数与 on 函数实现基本相同,可以实现一个通用的 bind 函数:

_bind(type: string, eventName: string, listener: Function, flag = false): EventEmitter{
    const event = type === "on" ? this.event : this.onceEvent;
    const fnAry = event[eventName];
    if(!fnAry){
        event[eventName] = [listener];
    }else{
        // 如果数组中没有监听函数,才添加
        if(!fnAry.includes(listener)){
            if(typeof listener === "function")
                flag ? event[eventName].unshift(listener)
                    : event[eventName].push(listener);
        }
    }
    return this;
}

on(eventName: string, listener: Function, flag = false): EventEmitter{
    return this._bind('on', eventName, listener, flag);
}
// 绑定某个事件函数,函数触发一次后就会被销毁
once(eventName: string, listener: Function, flag = false): EventEmitter{
    return this._bind('once', eventName, listener, flag);
}

ononce 函数都可以接受一个 flag 参数,默认是 false。当是 true 时,listener 函数会添加到数组的最前面(unshift 操作),在触发事件时会优先调用。

addEventListeneron 函数一样,只是个别称。

addEventListener(eventName: string, listener: Function, flag = false): EventEmitter{
    return this.on(eventName, listener, flag);
}

EventEmitter 类中,还提供了 prependListenerprependOnceListener 函数,它们是将监听函数添加到数组的开头位置,这两个函数分别对应于 ononce 两种绑定方式。

// 把 flag 参数设置成 true
prependListener(eventName: string, listener: Function): EventEmitter{
    return this.on(eventName, listener, true);
}
prependOnceListener(eventName: string, listener: Function): EventEmitter{
    return this.once(eventName, listener, true);
}

emit

触发事件,成功触发返回 true,否则返回 false。

_perform(fnAry: Function[], ...args: any[]){
    fnAry.forEach(fn => {
        fn.apply(this, args);
    });
}
emit(eventName: string, ...args: any[]): boolean{
    // 先触发 on 中的函数,再触发 once 中的函数
    const onEventFn = this.event[eventName];
    const onceEventFn = this.onceEvent[eventName];
    let flag = false;
    if(onEventFn){
        flag = true;
        this.perform(onEventFn, ...args);
    }
    if(onceEventFn){
        flag = true;
        this.perform(onceEventFn, ...args);
        // 执行完成后,别忘了销毁该事件监听
        delete this.onceEvent[eventName];
    }
    return flag;
}

removeEventListener

代码如下:

removeEventListener(eventName: string, listener: Function): EventEmitter{
    let onEventFn = this.event[eventName];
    let onceEventFn = this.onceEvent[eventName];
    if(onEventFn){
        // 过滤
        this.event[eventName] = onEventFn.filter(fn => && fn !== listener);
    }
    if(onceEventFn){
        this.onceEvent[eventName] = onceEventFn.filter(fn => && fn !== listener);
    }
    return this;
}

除了 removeEventListener 方法外,还可以实现一个 removeAllListener,表示移除 eventName 对应的全部的监听函数。

removeAllListener(eventName: string): EventEmitter{
    delete this.event[eventName];
    delete this.onceEvent[eventName];
    return this;
}

一个基本的发布订阅模式就实现了。

前端-自定义事件

说完了 EventEmitter,再来说说前端的自定义事件。

想对某个元素设置上自己定义的事件类型,如何做到?比如:

div.addEventListener("myEvent", (e) => {
    // ...
});

CustomEvent

使用如下:

// 初始化 event 对象
var event = new CustomEvent("color", {
    detail: {
        color: 'red'
    },
    bubbles: false,
    cancelable: true,
});

const div = document.getElementById("main");
// 绑定事件
div.addEventListener("color", function(e){
    console.log(e);
});

// 触发事件
div.dispatchEvent(event);

CustomEvent 构造函数接收两个参数,第一个是事件名称,第二个是一个配置参数,配置项有:

  • bubbles 一个布尔值,表明该事件是否会冒泡;
  • cancelable 一个布尔值,表明该事件是否可以被取消;
  • detail 当事件初始化时传递的数据;

通过 dom.dispatchEvent 方法触发事件。

bubbles 配置项是 true 时表明该事件会冒泡。

var event = new CustomEvent("color", {
    detail: {
        color: 'red'
    },
    bubbles: true,
    cancelable: true,
});
// 给 document 绑定事件
document.addEventListener("color", function(e){
    console.log(e);
});

// 由 div 触发事件
div.dispatchEvent(event);

上面代码,因为 color 事件可以冒泡,因此 div 元素也可以触发。

Event

Event 类与 CustomEvent 类相似。用法如下:

const div = document.getElementById("main");
var event = new Event('color', {
    "bubbles": true,
    "cancelable": true,
});

document.addEventListener("color", (e) => console.log(e));
div.dispatchEvent(event);
document.dispatchEvent(event);

CustomEvent 相比,Event 构造函数的第二个参数——配置项中似乎不能传递初始化数据(detail)。

前端的 addEventListener 函数也可以为元素注册一次性事件,在第三个参数中传入一个对象,将 once 设置成 true 即可。

div.addEventListener("click", () => {
    // ...
}, {
    once: true,      // 只调用一次
    capture: false, // 在冒泡阶段触发事件
});

你可能感兴趣的:(设计模式,Node.js,JavaScript)