一文获悉组件通信之——发布-订阅模式

利用“发布-订阅”模式驱动数据流

“发布-订阅”模式可谓是解决通信类问题的“万金油”,在前端世界的应用非常广泛,比如:

  • 前两年爆火的 socket.io 模块,它就是一个典型的跨端发布-订阅模式的实现;

  • Node.js 中,许多原生模块也是以 EventEmitter 为基类实现的;

  • 不过最为熟知的,应该还是 Vue.js 中作为常规操作被推而广之的“全局事件总线” EventBus

这些应用之间虽然名字各不相同,但内核是一致的,也就是下面要讲到的“发布-订阅”模型。

理解事件的发布-订阅机制

发布-订阅机制早期最广泛的应用,应该是在浏览器的 DOM 事件中。 相信有过原生 JavaScript 开发经验的同学,对下面这样的用法都不会陌生:

target.addEventListener(type, listener, useCapture);

通过调用 addEventListener 方法,可以创建一个事件监听器,这个动作就是“订阅”。比如我可以监听 click(点击)事件:

el.addEventListener("click", func, false);

这样一来,当 click 事件被触发时,事件会被“发布”出去,进而触发监听这个事件的 func 函数。这就是一个最简单的发布-订阅案例。

使用发布-订阅模式的优点在于,监听事件的位置和触发事件的位置是不受限的,只要它们在同一个上下文里,就能够彼此感知。这个特性,太适合用来应对“任意组件通信”这种场景了。

发布-订阅模型 API 设计思路

通过前面的讲解,不难看出发布-订阅模式中有两个关键的动作:事件的监听(订阅)和事件的触发(发布),这两个动作自然而然地对应着两个基本的 API 方法。

  • addListener():负责注册事件的监听器,指定事件触发时的回调函数。

  • emit():负责触发事件,可以通过传参使其在触发的时候携带数据 。

最后,只进不出总是不太合理的,还要考虑一个 removeListener() 方法,必要的时候用它来删除用不到的监听器:

  • removeListener():负责监听器的删除。

发布-订阅模型编码实现

在写代码之前,先要捋清楚思路。这里把“实现 EventEmitter ”这个大问题,拆解为 3 个具体的小问题,下面逐个来解决。

问题一:事件和监听函数的对应关系如何处理?

提到“对应关系”,应该联想到的是“映射”。在 JavaScript 中,处理“映射”大部分情况下都是用对象来做的。所以说在全局需要设置一个对象,来存储事件和监听函数之间的关系:

class EventBus {
    
    
    constructor() {
    
    
        // events 用来存储事件和监听函数之间的关系
        this.events = this.events || new Object();
    }
}

问题二:如何实现订阅?

所谓“订阅”,也就是注册事件监听函数的过程。这是一个“写”操作,具体来说就是把事件和对应的监听函数写入到 events 里面去:

EventBus.prototype.addListener = function (type, fun) {
    
    
    const e = this.events[type];

    if (!e) {
    
       //如果从未注册过监听函数,则将函数放入数组存入对应的键名下
        this.events[type] = [fun];
    } else {
    
      //如果注册过,则直接放入
        e.push(fun);
    }
};

问题三:如何实现发布?

订阅操作是一个“写”操作,相应的,发布操作就是一个“读”操作。发布的本质是触发安装在某个事件上的监听函数,需要做的就是找到这个事件对应的监听函数队列,将队列中的 fun 依次执行出队:

EventBus.prototype.emit = function (type, ...args) {
    
    
    let e;
    e = this.events[type];
    // 查看这个type的event有多少个回调函数,如果有多个需要依次调用。
    if (!e) {
    
    
        return
    } else if (Array.isArray(e)) {
    
    
        for (let i = 0; i < e.length; i++) {
    
    
            e[i].apply(this, args);
        }
    }
};

一个核心功能完备的 EventEmitter 如下:

class EventBus {
    
    
    constructor() {
    
    
        this.events = this.events || new Object();
    }
}
//首先构造函数需要存储event事件,使用键值对存储
//然后需要发布事件,参数是事件的type和需要传递的参数
EventBus.prototype.emit = function (type, ...args) {
    
    
    let e;
    e = this.events[type];
    // 查看这个type的event有多少个回调函数,如果有多个需要依次调用。
    if (!e) {
    
    
        return
    } else if (Array.isArray(e)) {
    
    
        for (let i = 0; i < e.length; i++) {
    
    
            e[i].apply(this, args);
        }
    }
};
//然后需要写监听函数,参数是事件type和触发时需要执行的回调函数
EventBus.prototype.addListener = function (type, fun) {
    
    
    const e = this.events[type];

    if (!e) {
    
       //如果从未注册过监听函数,则将函数放入数组存入对应的键名下
        this.events[type] = [fun];
    } else {
    
      //如果注册过,则直接放入
        e.push(fun);
    }
};
EventBus.prototype.removeListener = function (type) {
    
    
    delete this.events[type]
}
// 实例化
const eventBus = new EventBus();
export default eventBus;

测试

下面对 eventBus 进行一个简单的测试,针对名为 “test” 的事件进行监听和触发:

import eventBus from 'EventBus'

// 编写一个简单的 fun
const testHandler = function (params) {
    
    
    console.log(`test事件被触发了,testHandler 接收到的入参是${
      
      params}`);
};

// 监听 test 事件
eventBus.addListener("test", testHandler);

// 在触发 test 事件的同时,传入希望 testHandler 感知的参数
eventBus.emit("test", "newState");

以上代码会输出下面红色矩形框住的部分作为运行结果:

在这里插入图片描述
由此可以看出,EventEmitter 的实例已经具备发布-订阅的能力,执行结果符合预期。

猜你喜欢

转载自blog.csdn.net/qq_16525279/article/details/127493819