又到了愉快的周末时间了,在下午基本都在刷知乎的情况下,还是需要争取一下十点中之前摸一篇博客出来的。时间不算很多了,大概把之前的笔记过一遍吧。五一放假估计也不会写博客了,这个月能摸三篇出来已经算是有进步了。
关于 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
类型为string
,data
将被作为处理事件的参数,类型为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 还不怎么会,还需要花一些时间去琢磨,最近想做的事情有点多,有点目不暇接的感觉,只能看最近需求多不多了。