events event module
events and EventEmitter
- node.js is an event driven asynchronous operation architecture with built-in events module
- The events module provides the EventEmitter class
- The instance object of this class has the general operation of a series of event model mechanisms for registering events, publishing events and deleting events
- Many built-in core modules in node.js inherit the EventEmitter class
- Therefore, developers do not need to introduce the events module separately.
EventEmitter common API
- on: register event listeners
- emit: trigger events and call each event listener synchronously in the order of registration
- once: register the event listener, but execute it only when the event is published for the first time, and then delete it
- off: remove listener
const EventEmitter = require('events') const ev = new EventEmitter() // on ev.on('Event 1', () => { console.log('Event 1 execution') }) const handler = () => { console.log('Event 1 execution---Sequence 2') } ev.on('Event 1', handler) // once ev.once('Event 2', () => { console.log('Event 2 execution') }) // emit ev.emit('Event 1') ev.emit('Event 1') ev.emit('Event 2') ev.emit('Event 2') // off ev.off('Event 1', handler) ev.emit('Event 1')
The registered processing function can also pass parameters:
const EventEmitter = require('events') const ev = new EventEmitter() ev.on('Event 1', (a, b) => { console.log(a, b) }) ev.emit('Event 1', 1, 2)
this in the event handler function (not the arrow function) points to the EventEmitter instance:
ev.on('Event 1', function() { console.log(this) console.log(this === ev) // true }) ev.on('Event 1', () => {}) ev.on('Event 2', () => {}) ev.emit('Event 1')
In the EventEmitter instance, you can view what events are listened to and the processing functions bound on each event, for example:
EventEmitter { _events: [Object: null prototype] { 'Event 1': [ [Function], [Function] ], 'Event 2': [Function] }, _eventsCount: 2, _maxListeners: undefined, [Symbol(kCapture)]: false }
Other built-in module usage events:
const fs = require('fs') const crs = fs.createReadStream() crs.on('data', () => { // .. })
Event driven mechanism
Node and JS have an Event Loop mechanism. After the event is triggered, the callback function will be added to the Event Loop. After the main thread code is executed, the callback function in the Event Loop will be executed according to the internal implementation mechanism, so as to realize asynchronous programming.
Publish and subscribe
Publish subscribe mode is a model that defines one to many dependencies between objects, and decouples different objects. Objects do not need to know each other.
What problems can publish and subscribe solve?
Due to the characteristics of the language itself, multiple asynchronous APIs may be used continuously when implementing a specific requirement. If the calls of these APIs depend on each other's call results, many callback nesting will occur.
This style of code writing and maintenance reading is a headache before Promise, and the publish and subscribe mode solves this problem.
Three roles
There are three main roles in this model:
- Subscriber (Subscribe)
- Publisher (Publish)
- Message dispatching center
Message scheduling center is the main difference between publish subscribe mode and observer mode. It is precisely because of its existence that the publisher and subscriber are completely decoupled.
Corresponding to Nodejs, it can be understood as the event queue in the Event Loop.
Because of this, we can think that the EventEmitter class is implemented based on publish and subscribe.
Workflow
The subscriber registers the event listener he wants to subscribe to in the message scheduling center. When the event is triggered, the publisher will publish the event to the scheduling center, and then the scheduling center will uniformly schedule the previously registered code of the subscriber.
Publish subscribe features
- Cache queue to store subscriber information
- The publisher has the ability to add and delete subscriptions
- Notify all subscribers in the queue to listen when the status changes
Difference from observer mode
- There is a scheduling center in the publish / subscribe mode, but not in the observer mode
- When the status changes, the publish and subscribe mode does not need to actively notify the subscriber, and the scheduling center decides how to execute the subscription content
Simple simulated publish subscribe mode
class PubSub { constructor() { this._events = {} } // register subscribe(event, callback) { if (!this._events[event]) { this._events[event] = [] } this._events[event].push(callback) } // release publish(event, ...args) { const items = this._events[event] if (items && items.length) { items.forEach(callback => { callback(...args) }) } } } const ps = new PubSub() ps.subscribe('Event 1', function () { console.log('Event 1 execution') }) ps.subscribe('Event 1', function () { console.log('Event 1 execution----2') }) ps.publish('Event 1') ps.publish('Event 1')
Source code analysis of EventEmitter class
Prepare code for debugging
// EventEmitter-source.js const EventEmitter = require('events') const ev = new EventEmitter() ev.on('Event 1', () => { console.log('Event 1 execution') }) const handler = (...data) => { console.log('Event 1 execution', ...data) } ev.on('Event 1', handler) ev.emit('Event 1', 1, 2) ev.off('Event 1', handler)
vscode debug profile
{ // Use IntelliSense to understand related properties. // Hover to view the description of an existing property. // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 "version": "0.2.0", "configurations": [ { "type": "node", "request": "launch", "name": "Launch Program", // Ignore debugged files "skipFiles": [ // "< node_internal > / * *" is the node internal module // This example needs to analyze the source code, so you can't ignore it and comment it // Note: skipFiles cannot be commented directly, otherwise vscode will still adopt the default value // "<node_internals>/**" ], // After starting the program, modify the file to be opened to the correct address "program": "${workspaceFolder}\\EventEmitter-source.js" } ] }
EventEmitter constructor
function EventEmitter(opts) { EventEmitter.init.call(this, opts); }
EventEmitter.init
EventEmitter.init = function(opts) { // Initialization_ events if (this._events === undefined || this._events === ObjectGetPrototypeOf(this)._events) { // Define an empty object without a prototype to reduce resource consumption this._events = ObjectCreate(null); this._eventsCount = 0; } // Maximum number of listening events allowed to register this._maxListeners = this._maxListeners || undefined; if (opts && opts.captureRejections) { if (typeof opts.captureRejections !== 'boolean') { throw new ERR_INVALID_ARG_TYPE('options.captureRejections', 'boolean', opts.captureRejections); } this[kCapture] = Boolean(opts.captureRejections); } else { // Assigning the kCapture property directly saves an expensive // prototype lookup in a very sensitive hot path. this[kCapture] = EventEmitter.prototype[kCapture]; } };
on / addListener
ev.on actually calls addListener
EventEmitter.prototype.addListener = function addListener(type, listener) { // type event name; listener event callback return _addListener(this, type, listener, false); };
_addListener:
// target: EventEmitter instance object // type: event name // listener: event callback // prepend: where to insert the queue (before / after) function _addListener(target, type, listener, prepend) { let m; let events; let existing; // Monitors whether the listener format is a function checkListener(listener); // Get_ events events = target._events; if (events === undefined) { events = target._events = ObjectCreate(null); target._eventsCount = 0; } else { // To avoid recursion in the case that type === "newListener"! Before // adding it to the listeners, first emit "newListener". // If the newListener event is registered, the newListener event will be triggered when a new event is registered if (events.newListener !== undefined) { target.emit('newListener', type, listener.listener ? listener.listener : listener); // Re-assign `events` because a newListener handler could have caused the // this._events to be assigned to a new object events = target._events; } // Get the event object existing = events[type]; } // Judge whether the event has been subscribed if (existing === undefined) { // Optimize the case of one listener. Don't need the extra array object. // Add event callback // The first callback of the registered event will be directly stored in the event object, so the typeof existing === 'function' will be judged when adding again events[type] = listener; // Event count ++target._eventsCount; } else { // Add event callback if (typeof existing === 'function') { // Adding the second element, need to change to array. existing = events[type] = prepend ? [listener, existing] : [existing, listener]; // If we've already got an array, just append. } else if (prepend) { existing.unshift(listener); } else { existing.push(listener); } // Check for listener leak m = _getMaxListeners(target); // Handling when the number of registered events exceeds the limit if (m > 0 && existing.length > m && !existing.warned) { //... } } // Return instance object return target; }
emit
// type: event name // args: event callback parameter EventEmitter.prototype.emit = function emit(type, ...args) { let doError = (type === 'error'); // Get_ events const events = this._events; if (events !== undefined) { // error event handling if (doError && events[kErrorMonitor] !== undefined) this.emit(kErrorMonitor, ...args); doError = (doError && events.error === undefined); } else if (!doError) return false; // If there is no 'error' event listener then throw. // error event handling if (doError) { //... } // Get all callbacks for the event const handler = events[type]; if (handler === undefined) return false; // When you add event listening for the first time, callbacks will be stored directly instead of arrays, so you need to judge if (typeof handler === 'function') { // Call callback const result = ReflectApply(handler, this, args); // We check if result is undefined first because that // is the most common case so we do not pay any perf // penalty if (result !== undefined && result !== null) { addCatch(this, result, type, args); } } else { const len = handler.length; const listeners = arrayClone(handler); // Call callbacks are traversed in the order of registration for (let i = 0; i < len; ++i) { const result = ReflectApply(listeners[i], this, args); // We check if result is undefined first because that // is the most common case so we do not pay any perf // penalty. // This code is duplicated because extracting it away // would make it non-inlineable. if (result !== undefined && result !== null) { addCatch(this, result, type, args); } } } // The emit trigger event returns true instead of undefined return true; };
off / removeListener
ev.off actually calls removeListener
EventEmitter.prototype.off = EventEmitter.prototype.removeListener;
EventEmitter.prototype.removeListener = function removeListener(type, listener) { // Verify whether the listening event format is a function checkListener(listener); // Get event store object const events = this._events; if (events === undefined) return this; const list = events[type]; if (list === undefined) return this; if (list === listener || list.listener === listener) { // If only one listening callback has been registered for this event, and it is the one to be deleted // Update event count and judge if (--this._eventsCount === 0) // Reset_ events this._events = ObjectCreate(null); else { // Delete this event delete events[type]; // The removeListener event is triggered if (events.removeListener) this.emit('removeListener', type, list.listener || listener); } } else if (typeof list !== 'function') { // If this event is bound to multiple listening callbacks let position = -1; // Gets the location of the callback to delete for (let i = list.length - 1; i >= 0; i--) { if (list[i] === listener || list[i].listener === listener) { position = i; break; } } if (position < 0) return this; // Remove listening callback if (position === 0) list.shift(); else { if (spliceOne === undefined) spliceOne = require('internal/util').spliceOne; spliceOne(list, position); } // If there is only one listening callback, it is stored directly if (list.length === 1) events[type] = list[0]; // The removeListener event is triggered if (events.removeListener !== undefined) this.emit('removeListener', type, listener); } // Return instance object return this; };
summary
EventEmitter mainly uses four parts:
- When new is instantiated, it mainly creates an empty object. The empty object is created by null and has no prototype, which improves performance
- The on operation is called like addListener_ AddListener is mainly used to judge whether an event exists internally, so as to decide how to add event listening. If it is not registered, it will directly store the listening callback. If it is registered, it will store all callbacks in the array for maintenance
- emit executes the event. It mainly determines whether the callback bound to the event is one (function) or multiple (array), so as to decide how to call each callback
- removeListener deletes a subscription. Internally, it mainly determines whether the callback of event binding is one (function) or multiple (array), so as to decide how to delete it
EventEmitter simulation
Simply simulate the EventEmitter class to learn the native principle:
function MyEvent() { // Prepare a data structure for caching subscriber information this._events = Object.create(null) } MyEvent.prototype.on = function (type, callback) { // Judge whether the current event has been subscribed, so as to decide how to cache it if (this._events[type]) { this._events[type].push(callback) } else { this._events[type] = [callback] } } MyEvent.prototype.emit = function (type, ...args) { if (this._events[type] && this._events[type].length) { this._events[type].forEach(callback => { callback(...args) }) } } MyEvent.prototype.off = function (type, callback) { if (this._events[type] && this._events[type].length) { this._events[type] = this._events[type].filter(item => item !== callback && item.link !== callback) } } MyEvent.prototype.once = function (type, callback) { let foo = (...args) => { callback(...args) this.off(type, foo) } // Associate foo and callback. Callback can be matched when off foo.link = callback this.on(type, foo) } const ev = new MyEvent() ev.on('Event 1', () => { console.log('Event 1 execution') }) const handler = (...data) => { console.log('Event 1 execution', ...data) } ev.on('Event 1', handler) ev.emit('Event 1', 1, 2) ev.off('Event 1', handler) ev.emit('Event 1', 3, 4) ev.once('Event 2', handler) ev.emit('Event 2', 'front') ev.emit('Event 2', 'after') ev.once('Event 3', handler) ev.off('Event 3', handler) ev.emit('Event 3', 'front')