从发布订阅模式入手读懂Node.js的EventEmitter源码
前面一篇文章setTimeout和setImmediate到底谁先执行,本文让你彻底理解Event Loop详细讲解了浏览器和Node.js的异步API及其底层原理Event Loop。本文会讲一下不用原生API怎么达到异步的效果,也就是发布订阅模式。发布订阅模式在面试中也是高频考点,本文会自己实现一个发布订阅模式,弄懂了他的原理后,我们就可以去读Node.js的EventEmitter
源码,这也是一个典型的发布订阅模式。
本文所有例子已经上传到GitHub,同一个repo下面还有我所有博文和例子:
为什么要用发布订阅模式
在没有Promise
之前,我们使用异步API的时候经常会使用回调,但是如果有几个互相依赖的异步API调用,回调层级太多可能就会陷入“回调地狱”。下面代码演示了假如我们有三个网络请求,第二个必须等第一个结束才能发出,第三个必须等第二个结束才能发起,如果我们使用回调就会变成这样:
const request = require("request");
request('https://www.baidu.com', function (error, response) {
if (!error && response.statusCode == 200) {
console.log('get times 1');
request('https://www.baidu.com', function(error, response) {
if (!error && response.statusCode == 200) {
console.log('get times 2');
request('https://www.baidu.com', function(error, response) {
if (!error && response.statusCode == 200) {
console.log('get times 3');
}
})
}
})
}
});
复制代码
由于浏览器端ajax会有跨域问题,上述例子我是用Node.js运行的。这个例子里面有三层回调,我们已经有点晕了,如果再多几层,那真的就是“地狱”了。
发布订阅模式
发布订阅模式是一种设计模式,并不仅仅用于JS中,这种模式可以帮助我们解开“回调地狱”。他的流程如下图所示:
- 消息中心:负责存储消息与订阅者的对应关系,有消息触发时,负责通知订阅者
- 订阅者:去消息中心订阅自己感兴趣的消息
- 发布者:满足条件时,通过消息中心发布消息
有了这种模式,前面处理几个相互依赖的异步API就不用陷入"回调地狱"了,只需要让后面的订阅前面的成功消息,前面的成功后发布消息就行了。
自己实现一个发布订阅模式
知道了原理,我们自己来实现一个发布订阅模式,这次我们使用ES6的class来实现,如果你对JS的面向对象或者ES6的class还不熟悉,请看这篇文章:
class PubSub {
constructor() {
// 一个对象存放所有的消息订阅
// 每个消息对应一个数组,数组结构如下
// {
// "event1": [cb1, cb2]
// }
this.events = {}
}
subscribe(event, callback) {
if(this.events[event]) {
// 如果有人订阅过了,这个键已经存在,就往里面加就好了
this.events[event].push(callback);
} else {
// 没人订阅过,就建一个数组,回调放进去
this.events[event] = [callback]
}
}
publish(event, ...args) {
// 取出所有订阅者的回调执行
const subscribedEvents = this.events[event];
if(subscribedEvents && subscribedEvents.length) {
subscribedEvents.forEach(callback => {
callback.call(this, ...args);
});
}
}
unsubscribe(event, callback) {
// 删除某个订阅,保留其他订阅
const subscribedEvents = this.events[event];
if(subscribedEvents && subscribedEvents.length) {
this.events[event] = this.events[event].filter(cb => cb !== callback)
}
}
}
复制代码
解决回调地狱
有了我们自己的PubSub
,我们就可以用它来解决前面的回调地狱问题了:
const request = require("request");
const pubSub = new PubSub();
request('https://www.baidu.com', function (error, response) {
if (!error && response.statusCode == 200) {
console.log('get times 1');
// 发布请求1成功消息
pubSub.publish('request1Success');
}
});
// 订阅请求1成功的消息,然后发起请求2
pubSub.subscribe('request1Success', () => {
request('https://www.baidu.com', function (error, response) {
if (!error && response.statusCode == 200) {
console.log('get times 2');
// 发布请求2成功消息
pubSub.publish('request2Success');
}
});
})
// 订阅请求2成功的消息,然后发起请求3
pubSub.subscribe('request2Success', () => {
request('https://www.baidu.com', function (error, response) {
if (!error && response.statusCode == 200) {
console.log('get times 3');
// 发布请求3成功消息
pubSub.publish('request3Success');
}
});
})
复制代码
Node.js的EventEmitter
Node.js的EventEmitter
思想跟我们前面的例子是一样的,不过他有更多的错误处理和更多的API,源码在GitHub上都有:github.com/nodejs/node…。我们挑几个API看一下:
构造函数
代码传送门: github.com/nodejs/node…
构造函数很简单,就一行代码,主要逻辑都在EventEmitter.init
里面:
EventEmitter.init
里面也是做了一些初始化的工作,this._events
跟我们自己写的this.events
功能是一样的,用来存储订阅的事件。核心代码我在图上用箭头标出来了。这里需要注意一点,如果一个类型的事件只有一个订阅,this._events
就直接是那个函数了,而不是一个数组,在源码里面我们会多次看到对这个进行判断,这样写是为了提高性能。
订阅事件
代码传送门: github.com/nodejs/node…
EventEmitter
订阅事件的API是on
和addListener
,从源码中我们可以看出这两个方法是完全一样的:
这两个方法都是调用了_addListener
,这个方法对参数进行了判断和错误处理,核心代码仍然是往this._events
里面添加事件:
发布事件
代码传送门:github.com/nodejs/node…
EventEmitter
发布事件的API是emit
,这个API里面会对"error"类型的事件进行特殊处理,也就是抛出错误:
如果不是错误类型的事件,就把订阅的回调事件拿出来执行:
取消订阅
代码传送门:github.com/nodejs/node…
EventEmitter
里面取消订阅的API是removeListener
和off
,这两个是完全一样的。EventEmitter
的取消订阅API不仅仅会删除对应的订阅,在删除后还会emit一个removeListener
事件来通知外界。这里也会对this._events
里面对应的type
进行判断,如果只有一个,也就是说这个type
的类型是function
,会直接删除这个键,如果有多个订阅,就会找出这个订阅,然后删掉他。如果所有订阅都删完了,就直接将this._events
置空:
总结
本文讲解了发布订阅模式的原理,并自己实现了一个简单的发布订阅模式。在了解了原理后,还去读了Node.js的EventEmitter
模块的源码,进一步学习了生产环境的发布订阅模式的写法。总结下来发布订阅模式有以下特点:
- 解决了“回调地狱”
- 将多个模块进行了解耦,自己执行时,不需要知道另一个模块的存在,只需要关心发布出来的事件就行
- 因为多个模块可以不知道对方的存在,自己关心的事件可能是一个很遥远的旮旯发布出来的,也不能通过代码跳转直接找到发布事件的地方,debug的时候可能会有点困难。
文章的最后,感谢你花费宝贵的时间阅读本文,如果本文给了你一点点帮助或者启发,请不要吝啬你的赞和GitHub小星星,你的支持是作者持续创作的动力。
作者博文GitHub项目地址: github.com/dennis-jian…
作者掘金文章汇总:juejin.im/post/684490…
- JavaScript 教程
- JavaScript 编辑工具
- JavaScript 与HTML
- JavaScript 与Java
- JavaScript 数据结构
- JavaScript 基本数据类型
- JavaScript 特殊数据类型
- JavaScript 运算符
- JavaScript typeof 运算符
- JavaScript 表达式
- JavaScript 类型转换
- JavaScript 基本语法
- JavaScript 注释
- Javascript 基本处理流程
- Javascript 选择结构
- Javascript if 语句
- Javascript if 语句的嵌套
- Javascript switch 语句
- Javascript 循环结构
- Javascript 循环结构实例
- Javascript 跳转语句
- Javascript 控制语句总结
- Javascript 函数介绍
- Javascript 函数的定义
- Javascript 函数调用
- Javascript 几种特殊的函数
- JavaScript 内置函数简介
- Javascript eval() 函数
- Javascript isFinite() 函数
- Javascript isNaN() 函数
- parseInt() 与 parseFloat()
- escape() 与 unescape()
- Javascript 字符串介绍
- Javascript length属性
- javascript 字符串函数
- Javascript 日期对象简介
- Javascript 日期对象用途
- Date 对象属性和方法
- Javascript 数组是什么
- Javascript 创建数组
- Javascript 数组赋值与取值
- Javascript 数组属性和方法
- sqlmap的使用方法
- 从0开始做播放器---音频播放有杂音且音调异常
- 线上故障实录-一大早服务就不可用了?
- mapboxGL中popup遮挡的优化
- SQL注入的基本步骤
- JS 变量作用域导致的一个坑
- 池化技术到达有多牛?看了线程和线程池的对比吓我一跳!
- Nginx 跨域 add_header 403状态下无效
- Cannot set property 'branchdata' of undefined
- 【每日一题】27. Remove Element
- 【CPP】《程序员面试金典》习题(1)——数组与字符串
- 【CPP】《程序员面试金典》习题(2)——链表
- 【CPP】《程序员面试金典》习题(3)——栈和队列
- PPYOLO:2020不容错过的目标检测调参Tricks
- 【笔记】《C++Primer》—— 第11章:关联容器