Node.js basic design pattern --- observer pattern

Posted by portabletelly on Fri, 04 Mar 2022 22:58:09 +0100

introduction:

On node Another important and basic pattern used in JS is the observer pattern, which is also one of the pillars of the platform and a prerequisite for using the node core and user module

The observer defines an ideal solution for building nodes JS response characteristics, and is a perfect complement to callback. A formal definition is given below:

The observer pattern defines an object (called a subject) that can notify a group of observers (or listeners) when its reaction state changes

The main difference from the callback mode is that the subject can actually notify multiple observers, while the traditional CPS callback usually propagates its results to one listener, that is, the callback

EventEmitter class

In traditional object-oriented programming, the observer pattern requires interfaces, entity classes and hierarchies JS, everything becomes simple. The observer mode is built into the kernel and can be obtained through the EventEmitter class. EventEmitter class allows us to register one or more functions as listeners. When a specific time type is triggered, it will be called:

 ┌───────────────────────────────────────────────────────────┐
 │                                                           │
 │                                        ┌────────────┐	 │
 │										  │			   │	 │
 │                                        │            │	 │
 │										  │			   │	 │
 │                                    ┌──►│  Listener  │	 │
 │									  │   │            │	 │
 │                   ┌─┬────────────┐ │   │            │	 │
 │				     │ │ 			│ │   │            │     │
 │     ┌─────────────┼─┤            │ │   └────────────┘     │
 │     │             │ │  EventA    ├─┤                      │
 │     │             │ │            │ │   ┌────────────┐     │
 │     │             └─┼────────────┘ └──►│            │     │
 │     │  EventEmitter │                  │  Listener  │     │
 │     │             ┌─┼────────────┐ ┌──►│            │     │
 │     │             │ │            │ │   └────────────┘     │
 │     │             │ │  EventB    ├─┤                      │
 │     └─────────────┼─┤            │ │   ┌────────────┐	 │
 │					 │ │			│ │   │            │     │
 │                   └─┴────────────┘ └───►            │     │
 │										  │ 		   │     │
 │                                        │  Listener  │	 │
 │										  │			   │     │
 │                                        │            │	 │
 │										  │			   │	 │
 │                                        └────────────┘     │
 │                                                           │
 └───────────────────────────────────────────────────────────┘

The EventEmitter class can be obtained from the events module provided by node

let EventEmitter = require('events').EventEmitter

Here are some common methods

  • on
  • once
  • emit
  • removeListner

The method of EventEmitter is described in detail on the official website http://nodejs.cn/api/events.html

Create and use EventEmitter

let EventEmitter = require('events').EventEmitter
let fs = require('fs')

function findPattern(files, regex) {
    let emitter = new EventEmitter()
    files.forEach(file => {
        fs.readFile(file, 'utf8', (err, result) => {   
            if (err) {
                return emitter.emit('error', err) 	//When the file cannot be found, register and trigger the err event to pass the error to the listener
            }
            emitter.emit('fileread', file)   //Register and trigger the fileread event to pass the file name to the listener
            let match
            if (match = result.match(regex)) {
                emitter.emit('find', match)		//Register and trigger the find event to pass the content to the listener
            }
        })
    });
    return emitter
}
findPattern(
        ['fileA.txt', 'fileB.json', 'df.json'],   
        /hello \w+/g
    )
    .on('fileread', file => console.log(file + ' was read )  //Listen to the fileread event. File receives the file name of the open file
    .on('find', match => console.log(match + ' was find))   //Listen for the find time. match is the string array found by regular matching
    .on('error', err=> console.log(err+ ' happend))   //Listen for the find time. match is the string array found by regular matching

Make any object observable

Sometimes, it is not enough to create a new observable object directly from the EventEmitter class, because it is impractical to provide functions other than generating new events. It is easy to expand the EventEmitter through the inherits function provided by the core module util

class filePatten extends EventEmitter {
    constructor(regex) {
        super()
        this.files = []
        this.regex = regex
    }
    add(fileName) {
        this.files.push(fileName)
        return this
    }
    find() {
        this.files.forEach(file => {
            fs.readFile(file, 'utf8', (err, result) => {
                if (err) {
                    this.emit('error', err)
                }
                this.emit('fileread', file)
                let match
                if (match = result.match(this.regex)) {
                    this.emit('find', match)
                }
            })
        })
        return this
    }
}
let mode = new filePatten(/hello \w+/g)
mode.add('fileA.txt')
    .add('fileB.json')
    .find()
    .on('fileread', file => console.log(file + 'has read'))
    .on('find', match => console.log(match))

By inheriting the function of EventEmitter, we can see how the filePatten object has a complete set of methods. In order to keep similar to the methods of EventEmitter class, we let the custom methods return themselves, return this, and keep the style consistent, because EventEmitter prototpye. On and other methods also return this by default

Listen to the event before triggering

About the above code, you need to pay attention to one detail, that is The on method is first due to emit() is executed due to FS Readfile is an asynchronous function, which is monitored first and triggered later. Only in this way can our code successfully listen to events and execute callback functions. This is an error prone point. Next, we will give an error use case

Synchronous and asynchronous events

Like callbacks, events can be issued synchronously or asynchronously, which is Node.js basic design pattern callback pattern It is mentioned in the CPS in this article, but it is important not to mix the two methods in the same EventEmitter

The main difference between sending synchronous events and sending asynchronous events lies in the way of listener registration. When events are sent asynchronously, that is, after EventEmitter is initialized, the program still has events to register new listeners (this is what we discussed earlier, and the registered events are in asynchronous functions)

Instead, sending events synchronously requires registering all listeners before the EventEmitter function starts issuing any events. Let's see an example of the opposite

let EventEmitter = require('events').EventEmitter

class syncEmit extends EventEmitter{
	constructor() {
		super()
		this.emit('init') //Synchronously register and trigger the init event, but there is no observer listening to this event at this time
	}
}
let sync = new syncEmit().on('init',() => console.log('init success'))  // Therefore, this statement will not be output

A little modification

let EventEmitter = require('events').EventEmitter

class syncEmit extends EventEmitter{
	constructor() {
		super()
	}
}
let syncobj= new syncEmit().on('init',() => console.log('init success')) 

syncobj.emit('init')  //=>'init success' is triggered after listening

Choose event emitter or callback mode?

When defining asynchronous API s, the common difficulty is how to judge whether EventEmitter or callback should be used. The general principle is: when the result must be returned asynchronously, callback should be used; Use events when you need to communicate what just happened.

However, in addition to this simple principle, since most of the events in the two examples have the same effect and can achieve the same effect, there is a lot of confusion, such as:

function helloEvents() {
    let eventEmitter = new EventEmitter()
    setTimeout(() => {
        eventEmitter.emit('sayhello', 'hello world')
    })
}

function helloCallback(callback) {
    setTimeout(() => {
        callback('hello world')
    })
}

The two functions helloEvents() and helloCallback() can be considered functionally equivalent.

  • The first uses events to convey the completion of the timeout
  • The second uses a callback to notify the caller

As a first observation, we can say that callbacks have some limitations when supporting different types of events. In fact, we can still pass the type as the parameter of the callback function, for example:

function helloCallback(callback) {
    //...
    callback('init')    //Callback init event
    //...
    callback('read')	//Callback read event
    //...
    callback('end')		//Callback end event
    //...
}
function fun(type) {
    switch(type) {  //Processing in callback function
        case 'init' :
            //...
        case 'read' :
            //...
    }
}
helloCallback(fun)

Or receive several callbacks to distinguish different events by calling:

function helloCallback(callbackInit, callbackRead, callbackEnd) {
    //...
    callbackInit()   //Callback init event
    //...
    callbackRead()	//Callback read event
    //...
    callbackEnd()   //Callback end event
    //...
}
helloCallback(fun1,fun2,fun3)

However, you can't think of this as an elegant API. In this case, it is obvious that EventEmitter can provide better structure and leaner code

Another case where EventEmitter is preferred is that the same event may occur multiple times or not at all. The callback function can only be called once, regardless of whether the operation is successful or not. In fact, there is a possible repetition, which makes us reconsider the semantic characteristics of the event, which is more like an event that must be communicated than a result. In this case, EventEmitter is the best choice

last

  • The API using callbacks can only notify specific callbacks, while the EventEmitter function can make multiple listeners receive the same notification

Topics: node.js