又到了愉快的周末时间了,在下午基本都在刷知乎的情况下,还是需要争取一下十点中之前摸一篇博客出来的。时间不算很多了,大概把之前的笔记过一遍吧。五一放假估计也不会写博客了,这个月能摸三篇出来已经算是有进步了。

关于 EventHub,之前的博客里面也提到过许多回。其实在【归纳】React 组件通信与 Redux,就已经基本实现了一个简单的 EventHub。不过作为一个经典的面试题,把整个解题的流程大概整理一下,而且还是之前没有用过的 TypeScript,还是很有必要的。

封面

解题思路

所谓 EventHub,就是发布订阅模式,也叫观察者模式,是软件设计模式的一种。在此种模式中,要想将它用到组件的通信中,大概可以这么理解,发送消息的组件需要「发布」事件,接受消息的组件则需要「订阅」事件。负责处理事件的对象就可以称之为事件中心,也就是 EventHub。

在解题之前需要明确解题思路。作为处理对象的事件中心,大抵需要这几种方法和属性,而且使用class实现最合适不过了:

  • on(eventName, callback):订阅;
  • off(eventName):取消订阅;
  • emit(evnetName):发布
  • cache[]:数据(存放事件的处理函数)

在代码开发完毕之后需要添加测试用例,并且让自己的测试用例通过,不断重构代码,来证明自己的解答是否正确。在添加测试用例时,可以使用console.assert()(内容为false时会报错)来判断自己的断言是否正确。

这次的 EventHub 使用 TypeScript 开发,运行 ts 需要安装 ts-node,使用这种方式可以很方便地运行 ts 代码:

$ ts-node src/index.ts

直接上代码

根据上面的解题思路,在 EventHub 中需要实现四个方法及属性:订阅、取消订阅、发布,以及用于存放事件处理函数的对象。代码如下:

class EventHub {
    // 数据
    private cache: { [key: string]: Array<(data?: unknown) => void> } = {};

    // 订阅
    on(eventName: string, fn: (data?: unknown) => void) {
        this.cache[eventName] = this.cache[eventName] || [];
        this.cache[eventName].push[fn]
    }

    // 发布
    emit(eventName: string, data?: unknown) {
        (this.cache[eventName] || []).forEach(fn => fn(data));
    }

    // 取消订阅
    off(eventName: string, fn: (data?: unknown) => void) {
        const index = (this.cache[eventName] || []).findIndex(item => fn);
        if(index === -1) return;
        this.cache[eventName].splice(index, 1);
    }
}

export default EventHub;

首先是cache,在 TypeScript 中,可以使用private关键字定义类的私有属性,表示属性仅属于这个类。这样这个属性就只能在这个类中被访问,在子类和类的实现的对象中都不能访问,在子类可以通过调用使用这个属性的方法来间接使用这个属性。而这里的cache是比较适合作为私有属性的。

cache的数据类型是对象,对象的键是string类型,用于放置已订阅的事件名;属性值是函数数组,函数是发布事件后需要调用的函数,函数可以接受参数,参数类型为unknown,返回void。TypeScript 3.0 引入了新的unknown类型,它是any类型对应的安全类型,之后的博客再来详细说明,这里就不赘述了。

接下来就是订阅事件方法on,订阅事件就是将事件名及对应的处理函数置入cache中。on接受两个参数,事件名eventName类型为string,处理函数fn可以接受参数,参数类型为unknown,返回void。已经订阅的事件名成为cache中的键名,对应的属性值的数组的值就是事件的处理函数,一个事件可以被订阅多次,这样处理函数也会被依次推入在cache对应的数组中。

而到了事件发布时,实际上就是将对应事件的的所有事件处理函数都调用一遍。发布事件方法emit接受两个参数,事件名eventName类型为stringdata将被作为处理事件的参数,类型为unknown

已经订阅的事件也可以取消。取消订阅的方法off接受两个参数,事件名eventName类型为string及处理函数fn。这时将会在cache在以eventName为键的属性值在的数组,搜寻需要取消订阅的处理函数fn,如果搜寻到将会从数组中移除fn,这样在下次事件发布时就不会再触发这个处理函数了。

测试用例

接下来就需要借助自己的测试用例完善代码。下面有三个测试用例,分别针对「EventHub创建实例」、「发布订阅事件」、「取消订阅事件」做出了断言。如果断言为假,console.assert()将会抛出错误。不过在本地测试用例暂时还没有跑通,可能还是因为 ts-node 还不怎么会用吧,有空的时候再回来研究一下到底是哪个地方还姿势不对吧。

import EventHub from '../src/index';

type TestCase = (message: string) => void;

const test1: TestCase = message => {
    const eventHub = new EventHub();
    console.assert(eventHub instanceof Object === true, 'eventHub 是一个对象');
    console.log(message);
}; 

const test2: TestCase = message => {
    const eventHub = new EventHub();
    let called = false;
    eventHub.on('xxx', y => {
        called = true;
        console.assert(y[0] === '信息1');
        console.assert(y[1] === '信息2');
    });
    eventHub.emit('xxx', ['信息1', '信息2']);
    console.assert(called);
    console.log(message);
};

const test3: TestCase = message => {
    const eventHub = new EventHub();
    let called = false;
    const fn1 = () => {
        called = true;
    };

    eventHub.on('yyy', fn1);
    eventHub.off('yyy', fn1);
    eventHub.emit('yyy');
    console.assert(called === false);
    console.log(message);
};

test1('EventHub 可以创建对象');
test2('事件发布之后,会触发之前订阅的时间处理函数');
test3('可以取消订阅');

以上就是本篇博客关于 TypeScript 手写 EventHub 源码的所有内容了,结果到了昨天的十点钟还是没有把这篇博客摸完,自己再本地试着跑这个测试用例也没有跑起来,实在是太水了,不过代码我相信大致是没有问题的。TypeScript 还不怎么会,还需要花一些时间去琢磨,最近想做的事情有点多,有点目不暇接的感觉,只能看最近需求多不多了。

封底