EventEmitter3 source code analysis and learning

background

Event listening is a very common situation in the development of front-end. The event listening method on DOM allows us to see the convenience of processing specific business logic through events.

In some specific business scenarios, third-party user-defined events have unique advantages when there are many levels, function calls are difficult and multiple responses are required - convenient calls, avoiding multi-layer nesting and reducing the coupling between components.

EventEmitter3 mentioned in this article is a typical third-party event library, which allows us to realize the communication between multiple functions and components through custom practice.

Overall structure diagram

The design of EventEmitter3 is relatively simple, and the specific structure can be seen in the following figure.

Next, we will introduce this structure according to the normal thinking of ordinary people.

Structure and function of each part

EE

function EE(fn, context, once) {
    this.fn = fn;
    this.context = context;
    this.once = once || false;
}

From the EE like code, we can clearly understand that the first parameter is the callback function, the second parameter is the context of the callback function, and the third parameter is an once flag bit. Because the code is simple, here is a brief introduction.

Prototype attribute

events

This method is used to store the entire event name of eventEmitter and the collection of callback functions. The initial value is undefined.

Prototype method

eventName

  • Function: returns the list of currently registered event names

  • Parameter: None

listeners

  • Function: returns all listening functions of an event name

  • Parameters: event -- event name, exists -- whether only exists or not

emit

  • Function: trigger an event

  • Parameters: event -- event name, a1~a5 -- parameters 1 ~ 5

on

  • Function: add a listening function for an event

  • Parameters: event -- event name, fn -- callback function, context -- context

once

  • Function: similar to on, except that this function will only be triggered once

  • Parameters: event -- event name, fn -- callback function, context -- context

removeListner

  • Function: removes the listener function of an event

  • Parameters: event -- event name, fn -- event listening function, context -- remove only event listening functions with matching context, and once -- remove only event listening functions with matching type

removeAllListener

  • Function: remove all listening functions at a certain time

  • Parameter: event -- event name

Learning ideas

Next, we will analyze the specific code from adding listening functions, event triggering and deleting listening functions, so as to understand the implementation idea of the library.

Event object

The specific code is as follows:

//A unit of a single event listener function
//
// @param {Function} fn Event handler to be called. Callback function
// @Param {mixed} context for function execution. Function execution context
// @param {Boolean} [once=false] Only emit once flag bit of whether to execute once
// @api private

unction EE(fn, context, once) {
   this.fn = fn;
   this.context = context;
   this.once = once || false;

This class is the smallest class used to store event listener functions in eventEmitter.

Add listener function

The specific code of the on function is as follows:

// Register a new EventListener for the given event.
// Registers an event listener function for the specified event
//
// @param {String} event Name of the event
// @param {Function} fn Callback function. Callback function
// @param {Mixed} [context=this] The context of the function
// @api public public API

EventEmitter.prototype.on = function on(event, fn, context) {
   var listener = new EE(fn, context || this)
       , evt = prefix ? prefix + event : event;

   if (!this._events) this._events = prefix ? {} : Object.create(null);
   if (!this._events[evt]) {
       this._events[evt] = listener;//It is stored as an event listening object for the first time
   } else {
       if (!this._events[evt].fn) {//For the third time and beyond, add event listening objects directly to the object array
           this._events[evt].push(listener);
       } else {//Convert the stored object and the new object into an event listening object array for the second time
           this._events[evt] = [
               this._events[evt], listener
           ];
       }
   }

   return this;

When we add function F to event E, the on method will be called. At this time, the on method will check the e attribute of prototype attribute events in eventEmitter.

  • When this attribute is undefined, the event object where the function is located is directly assigned to the evt attribute.

  • When the current value of the property is an object and its function fn is not equal to function F, it will be converted to an event object array containing the two event objects.

  • When this property is already an object array, you can directly add objects to the array through the push method.

prefix is used to judge whether the Object.create() method exists. If it does exist, call this method directly to create the attribute. Otherwise, add ~ before the attribute to avoid overwriting the original attribute.

The function implementation of once is basically the same as that of on, so it will not be analyzed here.

Trigger listener function

The emit function code is as follows:

// Emit an event to all registered event listeners.
// Trigger the registered event listener function
//
// @param {String} event The name of the event
// @returns {Boolean} Indication if we've emitted an event. If the event is triggered successfully, it returns true; otherwise, it returns false
// @api public public API

EventEmitter.prototype.emit = function emit(event, a1, a2, a3, a4, a5) {
    var evt = prefix ? prefix + event : event;

    if (!this._events || !this._events[evt]) return false;

    var listeners = this._events[evt]
        , len = arguments.length
        , args
        , i;

    if ('function' === typeof listeners.fn) {
        if (listeners.once) this.removeListener(event, listeners.fn, undefined, true);

        switch(len) {
            case 1:
                return listeners.fn.call(listeners.context), true;
            case 2:
                return listeners.fn.call(listeners.context, a1), true;
            case 3:
                return listeners.fn.call(listeners.context, a1, a2), true;
            case 4:
                return listeners.fn.call(listeners.context, a1, a2, a3), true;
            case 5:
                return listeners.fn.call(listeners.context, a1, a2, a3, a4), true;
            case 6:
                return listeners.fn.call(listeners.context, a1, a2, a3, a4, a5), true;
        }

        for(i = 1, args = new Array(len - 1); i < len; i++) {
            args[i - 1] = arguments[i];
        }

        listeners.fn.apply(listeners.context, args);
    } else {
        //For space reasons, the process of event triggering is omitted through circular call when the E attribute is an array
    }

    return true;
};

When we trigger event E, we only need to call the emit method. This method will automatically retrieve all event listening objects in event E, trigger all event listening functions, and remove the event listening functions added through once that only need to be triggered once.

Remove event listener function

The removeListener function code is as follows:

// Remove event listeners.
// Remove event listener function
//
// @param {String} event The event we want to remove
// @param {Function} fn The listener that we need to find
// @param {Mixed} context Only remove listeners matching this context
// @param {Boolean} once Only remove once listeners. Only the listener functions that match the once attribute specified by this parameter are removed
// @api public public API

EventEmitter.prototype.removeListener = function removeListener(event, fn, context, once) {
    var evt = prefix ? prefix + event : event;

    if (!this._events || !this._events[evt]) return this;

    var listeners = this._events[evt]
        , events = [];

    if (fn) {
        if (listeners.fn) {
            if (
                listeners.fn !== fn
                || (once && !listeners.once)
                || (context && listeners.context !== context)
            ) {
                events.push(listeners);
            }
        } else {
            //For space reasons, the code that facilitates the deletion of the listeners attribute lookup function is omitted
        }
    }

    //
    // Reset the array, or remove it completely if we have no more listeners.
    //
    if (events.length) {
        this._events[evt] = events.length === 1 ? events[0] : events;
    } else {
        delete this._events[evt];
    }

    return this;
};

The removeListener function is relatively simple to implement. When we need to remove a function of event E, it uses an event attribute to save the event listening object that does not need to be removed, then facilitates the whole event listening array (single object), and finally assigns the value of event attribute to e attribute, so as to overwrite the original attribute and achieve the purpose of deletion.

other

There are some other functions in the library. Since they do not have much impact on the understanding of the whole library, they are not explained here. If necessary, you can go to my library github warehouse View.

shortcoming

Although the code structure of eventEmitter is clear, there are still some problems. For example, as like as two peas in on and once, only one attribute is different, and the rest of the code is the same. In fact, a specific function can be extracted to process the property, and the call can be distinguished by attributes.

At the same time, there are a lot of duplicate code in the same function, such as emit, which can be further abstracted and sorted to make the code simpler.

summary

The third-party event library of eventEmitter is relatively simple in implementation, clear in structure and easy to read. It is recommended that those interested can spend about an hour to learn.

appendix

My github address - eventEmitter3 Official github address

Tags: Javascript

Posted on Wed, 29 Sep 2021 13:51:30 -0400 by mynameisbob