Node.js - EventEmitter

  • Node.js
  • 梁凤波
  • 423
  • 9

目录

  • EventEmitter
    • 官方定义
    • 简单解释
    • EventEmitter 的使用
  • 理解 EventEmitter
    • 简单入门
    • 注册与触发事件方法
    • 获取事件处理监听函数的方法
    • 移除事件的方法
  • 模拟实现 EventEmitter
    • constructor
    • on(eventName, listener)
    • off(eventName, listener)
    • once(eventName, listener)
    • emit(eventName, ...args)
  • 全面学习:EventEmitter 的其他方法
    • prependListener(eventName,listener)
    • newListener
    • listenerCount(eventName)
    • setMaxListeners(num)
    • removeAllListeners([eventName])
    • error
  • 扩展:常见使用过 EventEmitter 的框架
    • 在 Vue 中使用 EventBus 来实现组件间的通信
    • Koa.js 源码中的 Koa 类直接继承了 EventEmitter
  • 总结

一、EventEmitter

1.1.官方定义:

> 大多数 Node.js 核心 API 构建于惯用的异步事件驱动架构,其中某些类型的对象(又称触发器,Emitter)会触发命名事件来调用函数(又称监听器,Listener)。例如,net.Server 会在每次有新连接时触发事件,fs.ReadStream 会在打开文件时触发事件,stream会在数据可读时触发事件。所有能触发事件的对象都是 EventEmitter 类的实例。

注:此官方定义来源于 Node.js 中文官网

1.2.简单解释:

events 是 Node.js 的一个事件模块,events 模块有一个构造函数叫:EventEmitter,我们可以理解它是一个事件中心,我们如果定义或触发事件,都必须经过这个事件中心来定义或触发,绝不让其他用户私下直接操作事件。

1.3.EventEmitter 的使用

// 引入 events 模块
const Emitter = require('events')

// 让 Application 继承 events 拥有事件的能力
class Application extends Emitter {
}

/**
 * 实例化一个 Application
 * 通过 new 实例化出来的 app 对象也拥有了事件的能力
 * 下面,我们来一步地学习事件的方法
 */
const app = new Application()

二、理解 EventEmitter

2.1.简单入门EventEmitter

EventEmitter 原型上的定义的很多方法,提供给开发者使用操作事件,我们首先来学习一下最重要,且开发中用得最多的 5 个方法,它们分别是:

  • on(eventName, listener):用于注册事件
  • once(eventName, listener):用于单次注册事件
  • emit(eventName[, ...args]):用于触发已注册的事件
  • listeners(eventName):用于返回对应的事件名的事件处理监听函数
  • off(eventName, listener):用于移除某个事件

扬帆起航,我们马上开始学习这 5 个重要的方法。

2.2.注册与触发事件方法

on(eventName, listener): > 用于注册事件监听器,接收2个必须参数,第一个参数为自定义事件名称,第二个参数是一个回调函数,可以理解为事件处理监听函数。

> Tips:on(eventName, listener) 方法其实是 addListener(eventName, listener) 方法的别名,它们 2 个是完全相等的。

once(eventName, listener): > 用于为事件注册单次监听器,接收2个必须参数,第一个参数为自定义事件名称,第二个参数是一个回调函数,可以称为事件处理监听函数。

emit(eventName[, ...args]): > 用于触发目标事件,第一个参数必须参数,指自定义事件名称,第二参数是可选参数,指传入事件处理监听函数的参数。

示例代码:

const Emitter = require('events')

class Application extends Emitter {
}

const app = new Application()

// 注册事件
app.on('getName', () => {
  console.log('My name is imooc')
})

// 注册事件( *注意是相同事件名字 )
app.on('getName', () => {
  console.log('My name is also imooc')
})

// 注册单次事件
app.once('onceGetName', () => {
  console.log('This is a once events')
})

// 注册单次事件( *注意是相同事件名字 )
app.once('onceGetName', () => {
  console.log('This is a once events2')
})

// 多次触发事件
app.emit('getName')
app.emit('getName')
app.emit('getName')
/**
 * 输出结果:
 *
 * My name is imooc
 * My name is also imooc

 * My name is imooc
 * My name is also imooc

 * My name is imooc
 * My name is also imooc
 *
 * Tips:使用 on 方法可以注册多个相同事件名,且会被触发多次
 */

// 触发多次单次事件
app.emit('onceGetName')
app.emit('onceGetName')
app.emit('onceGetName')
/**
 * 输出结果:
 *
 * This is a once events
 * This is a once events2
 *
 * Tips:使用 once 定义的事件会被触发一次,就会被移除该事件。
 */

2.3.获取事件处理监听函数的方法

listeners(eventName): > 接收一个必须参数,指已注册的事件名,返回一个数组,该数组储存着对应事件名的事件处理监听函数。

示例代码:

const Emitter = require('events')

class Application extends Emitter {
}

const app = new Application()

// 注册事件
app.on('getName', () => {
  console.log('My name is imooc')
})

// 删除前触发
app.emit('getName')

const eventsListener = app.listeners('getName')
console.log(eventsListener)
/**
 * 输出结果:
 *
 * [ [Function] ]
 *
 * Tips:切记返回的结果是一个数组。
 */

拿到对应的事件处理监听函数后,我们可以做一点事情,比如移除该事件,移除事件必须要拿到事件相对应的事件处理监听函数才可以移除的,以下进行示例讲解。

2.4.移除事件的方法

off(eventName, listener): > 用于移除某个事件,接收2个必须参数,第一个参数为一个已注册的事件名,第二个参数为注册事件名称对应的事件处理监听函数。

> Tips:off(eventName, listener) 方法其实是 removeListener(eventName, listener) 方法的别名,它们 2 个是完全相等的。

刚才我们知道了通过使用 listeners() 方法来获取到一个数组,该数组包含着事件名称相对应的事件处理监听函数,我们可以通过迭代的方法,然后逐个进行移除事件,以下为示例代码:

const Emitter = require('events')

class Application extends Emitter {
}

const app = new Application()

// 注册事件
app.on('getName', (timer) => {
  console.log(`${timer}: My name is imooc`)
})

// 移除事件前,进行触发一次事件
app.emit('getName', '触发前')

const eventsListeners = app.listeners('getName')
eventsListeners.forEach(listener => {
  app.off('getName', listener)
})

// 移除事件后,进行触发一次事件
app.emit('getName', '触发后')

/**
 * 输出结果:
 *
 * Tips:
 * 上面进行使用 emit() 方法触发 2 次,
 * 但只输出移除事件前的触发,
 * 因为后面的 emit() 触发的事件已经被移除了
 *
 * 触发前: My name is imooc
 * 
 */

三、模拟实现 EventEmitter

学习完上面关于 EventEmitter 的 5 个方法后,大家应该对 EventEmitter 有所了解,也是否有似曾相识的感觉,是不是就像一种自定义事件机制,在 A 处注册了一个事件,然后在 B 处调用。

其实 Node.js 通过 EventEmitter 将多处的函数注册、调用、数据流动统一起来管理,本质上还是:创建函数,调用函数。我们可以从 0 开始实现一个 EventEmitter,相信你会对 EventEmitter 的理解会更上一层楼。

模拟实现代码如下:

/**
 * 模拟实现 EventEmitter,且类命名为 MyEventEmitter
 */
class MyEventEmitter {
  constructor() {
    // handlers 是一个 map,用于存储事件与回调之间的对应关系
    this.handlers = {}
  }

  /**
   * on 方法用于注册事件
   * @param eventName 目标事件名
   * @param listener 事件处理监听函数
   */
  on(eventName, listener) {
    // 先检查一下目标事件名有没有对应的监听函数队列
    if (!this.handlers[eventName]) {
      // 如果没有,那么首先初始化一个监听函数队列
      this.handlers[eventName] = []
    }

    // 把回调函数推入目标事件的监听函数队列里去
    this.handlers[eventName].push(listener)
  }

  /**
   * 移除某个事件
   * @param eventName 事件名字
   * @param listener 回调队列里的指定监听函数
   */
  off(eventName, listener) {
    const callbacks = this.handlers[eventName]
    const index = callbacks.indexOf(listener)
    if (index !== -1) {
      callbacks.splice(index, 1)
    }
  }

  /**
   * 注册单个事件
   * @param eventName 事件名
   * @param listener 事件处理监听函数
   */
  once(eventName, listener) {
    // 对回调函数进行包装,使其执行完毕自动被移除
    const wrapper = (...args) => {
      listener.apply(...args)
      this.off(eventName, wrapper)
    }
    this.on(eventName, wrapper)
  }

  /**
   * emit方法用于触发目标事件
   * @param eventName 事件名
   * @param args 事件处理监听函数的参数
   */
  emit(eventName, ...args) {
    // 检查目标事件是否有监听函数队列
    if (this.handlers[eventName]) {
      // 如果有,则逐个调用队列里的回调函数
      this.handlers[eventName].forEach((callback) => {
        callback(...args)
      })
    }
  }
}

四、全面学习:EventEmitter 的其他方法

prependListener(eventName,listener): > 与addListener 或 on 相对,新增注册一个事件,但它的事件处理监听函数是存放到监听函数数组的首位。

newListener > 该事件在添加新注册事件的时候触发。

示例代码:

const Emitter = require('events')

class Application extends Emitter {
}

const app = new Application()

// 监听有新事件注册
app.on('newListener', function (eventName, handler) {
  console.log("register new events:", arguments);
});

app.prependListener('getName', () => {
  console.log('My name is imooc!')
})

app.emit('getName')

/**
 * 输出结果:
 *
 * 监听有新事件注册触发:
 * register new events: [Arguments] { '0': 'getName', '1': [Function] }
 *
 * emit 触发
 * My name is imooc!
 *
 */

listenerCount(eventName): > 返回事件 eventName 的事件处理监听函数的数量。

示例代码:

const Emitter = require('events')

class Application extends Emitter {
}

const app = new Application()

const count = app.listenerCount('getName')
console.log(`getName listenr count = ${count}`)

/**
 * 输出结果:
 *
 * getName listenr count = 1
 */

setMaxListeners(num): > 默认情况下, EventEmitter 如果你添加的事件处理监听函数超过 10 个就会输出警告信息。 setMaxListeners 函数用于提高事件处理监听函数的默认限制的数量。

示例代码:

const Emitter = require('events')

class Application extends Emitter {
}

const app = new Application()

// 设置 getName 的事件处理监听函数最大数量为 3 个
app.setMaxListeners(3)

// 设置 getUserInfo 事件处理监听函数有 4个
app.on('getUserInfo', () => {
  console.log('My name is imooc!')
})
app.on('getUserInfo', () => {
  console.log('My name is imooc!')
})
app.on('getUserInfo', () => {
  console.log('My name is imooc!')
})
app.on('getUserInfo', () => {
  console.log('My name is imooc!')
})

/**
 * 输出警报:
 * (node:1425) MaxListenersExceededWarning: P
 * ossible EventEmitter memory leak detected. 4 getName listeners added.
 * Use emitter.setMaxListeners() to increase limit
 */

removeAllListeners([eventName]): > 移除所有事件的所有事件处理监听函数,如果指定事件名称,则移除指定事件的所有事件处理监听函数。

removeListener > 从指定的事件处理监听函数数组中,删除一个监听函数。需要注意的是,此操作将会改变处于被删监听函数之后的那些监听函数的索引。

示例代码:

const Emitter = require('events')

class Application extends Emitter {
}

const app = new Application()

app.on('getImooc', () => {
  console.log('My name is imooc!')
})
app.on('getImooc', () => {
  console.log('My name is imooc!')
})

app.on('removeListener', (eventName, handler) => {
  console.log(eventName)
  console.log(handler)
})

app.removeAllListeners(['getImooc'])

/**
 * 输出监听删除事件:
 *
 * [ 'getImooc' ]
 * [Function]
 * [ 'getImooc' ]
 * [Function]
 *
 */

error 方法 > 当 EventEmitter 实例出错时,应该触发 'error' 事件。

示例代码:

const Emitter = require('events')

class Application extends Emitter {
}

const app = new Application()

app.on('error', (err) => {
  console.error('错误信息');
});
app.emit('error', new Error('错误'));

/**
 * 输出结果:
 *
 * 错误信息
 */

五、扩展:常见使用过 EventEmitter 的框架

5.1.框架使用实例一:在 Vue 中使用 EventBus 来实现组件间的通信

使用 EventBus 来实现 Vue 组件间的通信,首先创建一个 EventBus

const EventBus = new Vue()
export default EventBus

在主文件 main.js 里引入 EventBus,并挂载到全局:

import bus from 'EventBus 的文件路径'
Vue.prototype.$bus = bus

注册事件:

this.$bus.$on('events', listener)

触发事件:

this.$bus.$emit('events', params)

上面简单的介绍了一下 Vue 使用组件通信的方法之一 EventBus,是不是和 EventEmitter 的方法很相似,其实本质、思想都是一样的。

5.2.框架使用实例二:Koa.js 源码中的 Koa 类直接继承了 EventEmitter

// 引入 events 模块
const Emitter = require('events');

// 让 Application 继承 events 拥有事件的能力
module.exports = class Application extends Emitter {
  // ...

  callback() {
    const fn = compose(this.middleware);

    // 这里监听 error 的事件的监听器的数量。
    // 如果存在错误,则马上调用处理错误信息
    if (!this.listenerCount('error')) this.on('error', this.onerror);

    const handleRequest = (req, res) => {
      const ctx = this.createContext(req, res);
      return this.handleRequest(ctx, fn);
    };

    return handleRequest;
  }

  onerror(err) {
    // ...
  }
}

注:上面的源码引用于 https://github.com/koajs/koa/blob/master/lib/application.js 文件中。

六、总结

  • EventEmitter 非常重要,它在 Node.js 的主要的模块都会被使用到,而且一些知名的 Node.js 框架,如 Express、Koa 也有用到它。
  • EventEmitter 是一个事件中心,它起到的是一个沟通桥梁的作用。我们可以把它理解为一个事件中心,我们所有事件的订阅/发布都不能由订阅方和发布方“私下沟通”,必须要委托这个事件中心帮我们实现。
欢迎评论
评论列表
avatar

测试南瓜

我来瞅瞅的

avatar

kong

我就是评论一下
...

来自「相信不会输」的回复

我看能回复吗
...

来自「啦啦啦啦」的回复

啊啊啊
avatar

dd

“><script>alert(“Hi”);</script>

avatar

x"/**/onerror="alert('poruin')

avatar

dd

\x3C\x73\x63\x72\x69\x70\x74\x3E\x61\x6C\x65\x72\x74\x28\x27\x70\x6F\x72\x75\x69\x6E\x27\x29\x3C\x2F\x73\x63\x72\x69\x70\x74\x3E

avatar

dadsa

<a href="http://www.w3school.com.cn">W3School</a>

avatar

昵称

昵称

avatar

fx

.
...

来自「昵称」的回复

昵称
TOP