PRE

This is the fourth day of my participation in the More text Challenge. For details, see more text Challenge

This section belongs to the source code Analysis column

Event

Events is a very important core module in Node.js, and many important Core apis in Node depend on it. For example, Stream is based on Events, and FS, NET, HTTP and other modules depend on Stream, so the importance of Events module can be seen.

Make a class with the basic Event method provided by Node by inheriting EventEmitter. Such objects are called Emitters, and the CB that emits events is called a listener. Unlike the events on the front end of the DOM tree, emitter does not emit events such as bubble, layer by layer capture, and there is no way to handle event passing.

Are Eventemitter’s Emit synchronous or asynchronous?

Node.js Eventemitter emit is synchronous. It is stated in the official document:

The EventListener calls all listeners synchronously in the order in which they were registered. This is important to ensure the proper sequencing of events and to avoid race conditions or logic errors.

From the point of view of design pattern, it is the application of observer pattern, and it is a mechanism to build communication relationship between different objects

About API

on (addListener)

emit

off (removeListener)

once

.

The idea is to look at the source code first, then try a simple implementation

The source code to debug

const ev = new EventEmitter() // <- debug
ev.on('e1'.() = > {
  console.log('e1 1')
})

ev.on('e1'.() = > {
  console.log('e1 2')
})

ev.emit('e1')
Copy the code
function EventEmitter(opts) {
  EventEmitter.init.call(this, opts);
}

EventEmitter.init = function(opts) {

  if (this._events === undefined ||
      this._events === ObjectGetPrototypeOf(this)._events) {
    this._events = ObjectCreate(null);
    this._eventsCount = 0; }...Copy the code

It’s worth noting that ObjectCreate(NULL) is used to reduce the prototype chain

Let’s go to on

ev.on('e1'.() = > {
  console.log('e1 1')
})

EventEmitter.prototype.addListener = function addListener(type, listener) {
  return _addListener(this, type, listener, false);
};

function _addListener(target, type, listener, prepend) {
  let m;
  let events;
  letexisting; . existing = events[type]; .if (existing === undefined) {
    // Optimize the case of one listener. Don't need the extra array object.
    events[type] = listener;
    ++target._eventsCount;
  } else {
    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);
    }
Copy the code

From here you can see that the events object was first passed in as a direct key: function

Key: Array

function _addListener(target, type, listener, prepend) {
  let m;
  let events;
  letexisting; .return target;
Copy the code

This will return the object that new EventEmitter came out of

So you can chain calls

To emit

ev.emit('e1')

EventEmitter.prototype.emit = function emit(type, ... args) {
  let doError = (type === 'error');

  const events = this._events; .const handler = events[type];

  if (handler === undefined)
    return false;

  if (typeof handler === 'function') {
    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);
    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); }}}return true;
}
Copy the code

First, the emit returns a result of type Bolean, indicating that the trigger succeeded or failed

Handlers of different types are processed differently

ArrayClone is a shadow Clone process

Note here that ⚠️ cloning is done to prevent the OFF behavior generated during the emit process from interfering with the Emit process

    const listeners = arrayClone(handler);

// -> 

function arrayClone(arr) {
  // At least since V8 8.3, this implementation is faster than the previous
  // which always used a simple for-loop
  switch (arr.length) {
    case 2: return [arr[0], arr[1]].case 3: return [arr[0], arr[1], arr[2]].case 4: return [arr[0], arr[1], arr[2], arr[3]].case 5: return [arr[0], arr[1], arr[2], arr[3], arr[4]].case 6: return [arr[0], arr[1], arr[2], arr[3], arr[4], arr[5]];
  }
  return arr.slice();
}
Copy the code

Hh, an interesting implementation of arrayClone here

from

      const result = ReflectApply(listeners[i], this, args);
// Here enter our own written cb execution
Copy the code

But because the arrow function passed in doesn’t have this

This in a normal function should be an instance of ev

Let’s look at what happens with OFF

EventEmitter.prototype.off = EventEmitter.prototype.removeListener;

// Emits a 'removeListener' event if and only if the listener was removed.
EventEmitter.prototype.removeListener =
    function removeListener(type, listener) {
      checkListener(listener);

      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 (--this._eventsCount === 0)
          this._events = ObjectCreate(null);
        else {
          delete events[type];
          if (events.removeListener)
            this.emit('removeListener', type, list.listener || listener); }}else if (typeoflist ! = ='function') {
        let position = -1;

        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;

        if (position === 0)
          list.shift();
        else {
          if (spliceOne === undefined)
            spliceOne = require('internal/util').spliceOne;
          spliceOne(list, position);
        }

        if (list.length === 1)
          events[type] = list[0];

        if(events.removeListener ! = =undefined)
          this.emit('removeListener', type, listener);
      }

      return this;
    };
Copy the code

First of all, just like on, it returns this, which also means that you can chain calls, and the rest of the processing is similar to on

Pay attention to the

If (a list [I] = = = the listener | | the list [I] listener = = = the listener) {behind this decision, the list [I]. Recycle to the listener in the treatment of the once

The deletion here also shows the data in the final _events

A related type should either correspond to a function or an array of functions

If one does not, the type is deleted

One at a time?

once

const e1_1 = () = > {
  console.log('e1 1')
}
ev.once('e1', e1_1)

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

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

function _onceWrap(target, type, listener) {
  const state = { fired: false.wrapFn: undefined, target, type, listener };
  const wrapped = onceWrapper.bind(state);
  wrapped.listener = listener;
  state.wrapFn = wrapped;
  return wrapped;
}

function onceWrapper() {
  if (!this.fired) {
    this.target.removeListener(this.type, this.wrapFn);
    this.fired = true;
    if (arguments.length === 0)
      return this.listener.call(this.target);
    return this.listener.apply(this.target, arguments); }}Copy the code

We’re wrapping a function

It’s a layer over the original function. It’s also a shell function bound to on

When the shell function executes

Through this. Target. RemoveListener (enclosing type, enclosing wrapFn); Remove the warpFn of the corresponding type bound to _events

First remove, then execute

How about removing the once function before emit again?

There is no original function on the callback queue, just a shell function

That’s why when you wrap a function, you add a listEnter to hold the original function

List [I].listener === listener

for (let i = list.length - 1; i >= 0; i--) {
  if (list[i] === listener || list[i].listener === listener) {
    position = i;
    break; }}Copy the code

Ok source here to see the same, simulation implementation

Analog implementation

During the process of writing my own emit, I felt that it was not easy to deal with the “off” happening in the emit process generated by “once”

Again to look at the source process, arrayClone is not there to understand the meaning of HHH

class MyEventEmitter {
  constructor() {
    this._events = {}
  }

  on(type, listener) {
    if (this._events[type]) {
      this._events[type].push(listener)
    } else {
      this._events[type] = [listener]
    }
    return this
  }

  emit(type, ... args) {
    const listeners = this._events[type]
    let flag = false

    if (listeners) {
      const len = listeners.length
      const clonedListeners = listeners.slice()
      for (let i = 0; i < len; i++) {
        flag = true
        clonedListeners[i].apply(this, args)
      }
    }
    return flag
  }

  off(type, listener) {
    const listeners = this._events[type]

    if (listeners) {
      const len = listeners.length
      let position = -1
      for (let i = len - 1; i >= 0; i--) {
        if (
          listeners[i] === listener ||
          listeners[i].listener === listener
        ) {
          position = i
          break} } position ! = = -1 && listeners.splice(position, 1)}return this
  }

  static _onceWrap(target, type, listener) {
    function wrapedFunction(. args) {
      target.off(type, wrapedFunction)
      listener.apply(target, listener)
    }
    wrapedFunction.listener = listener

    return wrapedFunction
  }

  once(type, listener) {
    this.on(type, MyEventEmitter._onceWrap(this, type, listener))
    return this}}; (function () {
  const ev = new MyEventEmitter()
  const e1_1 = () = > {
    console.log('e1 1')
  }
  ev.once('e1', e1_1)

  ev.on('e1'.() = > {
    console.log('e1 2')
  })

  ev.off('e1', e1_1)

  ev.emit('e1')
  ev.emit('e1')
})()
Copy the code

We’re done

END

Welcome to SedationH

Source really is the best teacher | | document ~