Life is a story. What matters is not how long it is, but how good it is. – Seneca

EventEmitter is an important module in Node.js, which is also called publish/subscribe. It is important because most modules in Node.js depend on it, such as Net, HTTP, FS, Stream, etc. In addition to the well-known system modules Express, EventEmitter can also be found in the Koa framework.

PreventDefault (), stopPropagation(), etc. EventEmitter provides methods such as on(), once(), and removeListener() to monitor and remove events.

About the author: May Jun, Nodejs Developer, moOCnet certified author, love technology, love to share the post-90s youth, welcome to pay attention to Nodejs technology stack and Github open source project www.nodejs.red

What can you learn from this article

  • What is EventEmitter? Some basic API usage
  • How can EventEmitter be used in some core modules of Node.js (Stream, Net)?
  • Mainstream Express/Koa frameworks are based on this implementation. How do we implement a custom object based on EventEmitter?
  • How to use EventEmitter characteristics to solve avalanche problems in high concurrency scenarios?
  • Are events equivalent to asynchrony?

Let’s start with a simple example

Event-driven is the core of Node.js. Usually one of the most common forms is a callback, which fires an event and then receives some processing through a callback, and this is a common form of JavaScript programming, For example, fs.readfile (path, callback), server.on(‘data’, callback) in TCP, etc.

A simple implementation

The following two apis are used to trigger and register a listener function.

  • Emit: Fires a listener function
  • On: Registers a listener function
const EventEmitter = require('events').EventEmitter;
const emitter = new EventEmitter();

emitter.on("Wake up".function(time) {
    console.log(Morning `${time}Get up and start the day! `)
    //console.log(' pay attention to the public Nodejs technology stack, ${time} point in the morning to get up and read from node.js technology stack ');
});

emitter.emit("Wake up"."6");
Copy the code

After running the program, it looks like this:

Wake up at 6:00 am, come on for the new day!Copy the code

In addition to using the emit and ON methods above, there are some useful apis that you may want to visit the Node.js website (nodejs.cn/api/events….) first. Just to give you an overview, it’s all covered there, and I’ll show you how some of the core apis are used in some of the examples that follow.

Customize the EventEmitter class

When you learn about EventEmitter, you’ll find it everywhere in Node.js, including the core module of Node.js, Express/Koa, and other well-known frameworks. For example, when creating an app object in Koa, Emit an event via app.emit() that is passed through the system.

const Koa = require('koa');
const app = new Koa();

app.on("koa".function() {
    console.log("Using EventEmitter in Koa");
});

app.emit("koa");
Copy the code

System module to define the implementation of EventEmitter class

Before we start, let’s look at how the Stream and Net modules in Node.js are implemented.

Implementation in the Stream module

// https://github.com/nodejs/node/blob/v10.x/lib/internal/streams/legacy.js#L6

const EE = require('events');
const util = require('util');

function Stream() {
  EE.call(this);
}
util.inherits(Stream, EE);
Copy the code

Net module in the implementation

// https://github.com/nodejs/node/blob/v10.x/lib/net.js#L1121
const EventEmitter = require('events');
const util = require('util');

function Server(options, connectionListener) {
  if(! (this instanceof Server))
    return new Server(options, connectionListener);

  EventEmitter.call(this); . } util.inherits(Server, EventEmitter);Copy the code

Looking at the two custom EventEmitter implementations of the node.js module above, both have something in common that uses the util.inherits(constructor, superConstructor) method. This is the utility class in Node.js. This reminds me of a method function inherit(p) from the JavaScript Magisterium (Chapter 6, p. 122) that creates a new object through stereotype inheritance, and util.inherits is an inheritance between objects through stereotype replication.

For example, the util.inherits(Server, EventEmitter) function above inherits functions defined by EventEmitter in the prototype. This allows for methods like on and EMIT in EventEmitter event triggers. Instead of using util.inherits(), node.js now recommends using ES6’s class and extends keywords to get language level inheritance support. SetPrototypeOf () is used to implement inheritance in original JS, so in Node.js 12x you will see the following code implementation.

// https://github.com/nodejs/node/blob/v12.x/lib/net.js#L1142
function Server(options, connectionListener) {
  if(! (this instanceof Server))
    return new Server(options, connectionListener);

  EventEmitter.call(this); . }// https://github.com/nodejs/node/blob/v12.x/lib/net.js#L1188
Object.setPrototypeOf(Server.prototype, EventEmitter.prototype);
Object.setPrototypeOf(Server, EventEmitter);
Copy the code

Implement a custom class based on EventEmitter

Here’s an example of a day planner that shows how EventEmitter can listen for things by firing events at different times, based on a custom class.

How does our custom OneDayPlan inherit from EventEmitter

const EventEmitter = require('events');
const oneDayPlanRun = {
    "6": function() {
        console.log('It's 6:00 in the morning, get up, start a new day refueling! `);
    },
    "7": function() {
        console.log('It's 7:00 in the morning, have breakfast! `); }}function OneDayPlan() {
    EventEmitter.call(this);
}

Object.setPrototypeOf(OneDayPlan.prototype, EventEmitter.prototype);
Object.setPrototypeOf(OneDayPlan, EventEmitter);
Copy the code

Now let’s instantiate the custom OneDayPlan class above to trigger/listen for events

const oneDayPlan = new OneDayPlan();

oneDayPlan.on("6".function() {
    oneDayPlanRun["6"] (); }); oneDayPlan.on("7".function() {
    oneDayPlanRun["7"] (); });async function doMain() {
    oneDayPlan.emit("6");

    await sleep(2000); // Output every 2 seconds

    oneDayPlan.emit("7");
}

doMain();

async function sleep(s) {
    return new Promise(function(reslve) {
        setTimeout(function() {
            reslve(1);
        }, s);
    });
}
Copy the code

EventEmitter solves the high concurrency avalanche problem

Query the DB data to need, we usually call it the hot spot data, this data is usually to add a layer on the DB cache, but in high concurrency scenario, if the cache is failure, at this time there will be a lot of request directly into the database, create constant pressure to the database, to cache the avalanche of solution, There are better solutions available online, but in Node.js we can use the once() method provided by the Events module.

Once Method Introduction

When multiple events of the same name are fired, the listener added with once will execute only once and will touch the event associated with it after execution, equivalent to a combination of the on and removeListener methods.

proxy.once('I'm handsome'.function() {
    console.log('Once: I'm handsome! ');
});

proxy.on('I'm handsome'.function() {
    console.log('ON: I'm handsome! ');
});


proxy.emit('I'm handsome');
proxy.emit('I'm handsome');
proxy.emit('I'm handsome');
Copy the code

The “I am handsome” event is triggered three times. The “ON” method is obediently repeated three times, but the “once” method says “I know I am handsome” and I can only say it once.

Once: I'm handsome! On: I'm handsome! On: I'm handsome! On: I'm handsome!Copy the code

The once method is a combination of on and removeListener. You can also see github.com/nodejs/node… After receiving the message, the once method listens with the on method and removes the listener itself with removeListener in the onceWrapper method.

function onceWrapper(. args) {
  if (!this.fired) {
    this.target.removeListener(this.type, this.wrapFn);
    this.fired = true;
    return Reflect.apply(this.listener, this.target, args); }}function _onceWrap(target, type, listener) {
  var state = { fired: false.wrapFn: undefined, target, type, listener };
  var wrapped = onceWrapper.bind(state);
  wrapped.listener = listener;
  state.wrapFn = wrapped;
  return wrapped;
}

EventEmitter.prototype.once = function once(type, listener) {
  checkListener(listener);

  this.on(type, _onceWrap(this, type, listener));
  return this;
};
Copy the code

coded

Once method is used to push all the callback requests into the event queue, for the same file name query to ensure that there is only once in the process of the same query from the beginning to the end, if the DB query also avoids the database query overhead brought by repeated data. The code is written with reference to the simple Nodejs Events module, where FS is used for file query, and the same is true for DB. In addition, the name and status of triggered/monitored Events are saved in the form of status key value pair. Finally, it is suggested to clear them to avoid memory leakage caused by large objects.

const events = require('events');
const emitter = new events.EventEmitter();
const fs = require('fs');
const status = {};

const select = function(file, filename, cb) {
    emitter.once(file, cb);
    
    if (status[file] === undefined) {
        status[file] = 'ready'; // There is no default setting
    }
    if (status[file] === 'ready') {
        status[file] = 'pending';
        fs.readFile(file, function(err, result) {
            console.log(filename);
            emitter.emit(file, err, result.toString());
            status[file] = 'ready';
            
            setTimeout(function() {
                delete status[file];
            }, 1000); }); }}for (let i=1; i<=11; i++) {
    if (i % 2= = =0) {
        select(`/tmp/a.txt`.'a file'.function(err, result) {
            console.log('err: ', err, 'result: ', result);
        });
    } else {
        select(`/tmp/b.txt`.'b file'.function(err, result) {
            console.log('err: ', err, 'result: ', result); }); }}Copy the code

The console runs the above code to test. Although it initiated several file query requests, the FS module really only executed two times, respectively querying two files A and B. For the same request, repeated query under the same conditions was avoided by using the feature of event listener once.

B file err: null result: B err: NULL result: B err: null result: B err: null result: B err: null result: B err: null result: B err: Null result: B err: NULL result: B A file err: NULL result: a err: NULL result: a err: null result: a err: null result: a err: null result: aCopy the code

By default, EventEmitter prints a warning if more than 10 listeners are added to a particular event. However, not all events are limited to 10 listeners. The Emitters. SetMaxListeners () method can modify limits for specific EventEmitter instances.

(node:88835) Warning: Possible EventEmitter memory leak detected. 11 /tmp/b.txt listeners added. Use emitter.setMaxListeners() to increase limit
(node:88835) Warning: Possible EventEmitter memory leak detected. 11 /tmp/a.txt listeners added. Use emitter.setMaxListeners() to increase limit
Copy the code

EventEmitter loop call problem

As shown in the code below, try to analyze the output for the following two cases

const events = require('events');
const emitter = new events.EventEmitter();
const test = (a)= > console.log('test');

/ * * a * /
emitter.on('test'.function() {
    test();
    emitter.emit('test');
})

emitter.emit('test');

/ * * 2 * /
emitter.on('test'.function() {
    test();
    emitter.on('test', test);
})

emitter.emit('test');
Copy the code

Example 1: Because the emit event firing is performed in the listener function on, the stack overflows in an infinite loop.

Emitter. On (‘test’, test); This line simply adds an event listener to the current event callback.

Example 1: RangeError: Maximum Call Stack Size exceeded Example 2:test
Copy the code

Synchronous or asynchronous

Another question is is the event equal to asynchrony? The answer is not equal. Take a look at the following code example, which prints 111 and then 222. Why? From the official API, “EventEmitter synchronously calls all listeners in the order in which they were registered. So you have to make sure that events are ordered correctly and race conditions are avoided.”

const events = require('events');
const emitter = new events.EventEmitter();

emitter.on('test'.function(){
    console.log(111)}); emitter.emit('test');
console.log(222)

/ / output
/ / 111
/ / 222
Copy the code

It is also possible to use setImmediate() or process.nexttick () to switch to asynchronous mode, with the code shown below:

const events = require('events');
const emitter = new events.EventEmitter();

emitter.on('test'.function(){
    setImmediate((a)= > {
        console.log(111);
    });
});
emitter.emit('test');
console.log(222)

/ / output
/ / 222
/ / 111
Copy the code

Error handling

Last but not least, error handling is important in Node.js. Once an error is thrown, the process may exit automatically. The following code will cause the process to exit because the event trigger has an error message and there is no corresponding error listener.

const events = require('events');
const emitter = new events.EventEmitter();

emitter.emit('error'.new Error('This is a error'));
console.log('test');
Copy the code

Log.console. log(‘test’); console.log(‘test’); It’s not going to be enforced.

events.js:167
      throw er; // Unhandled 'error' event
      ^

Error: This is a error
Copy the code

As a best practice, you should always register listeners for ‘error’ events

const events = require('events');
const emitter = new events.EventEmitter();

emitter.on('error'.function(err) {
    console.error(err);
})

emitter.emit('error'.new Error('This is a error'));

console.log('test');
Copy the code
Error: This is a error
    at Object.<anonymous> ...
test
Copy the code

As shown in the code above, the error event is listened for after the first call, and the Node process does not exit automatically as in previous programs, console.log(‘test’); It is also working properly.

conclusion

Many successful node.js modules and frameworks are based on EventEmitter, and it’s useful to know how to use EventEmitter and when to use it.

EventEmitter is essentially an implementation of the observer model. A similar model is publish/subscribe, where producers publish messages without caring about the implementation of the subscriber. For those of you who have followed the Nodejs public number, you may have received my RabbitMQ series. RabbitMQ itself is based on the AMQP protocol, which is also a good solution to use in a distributed cluster environment.