Scenario

The following scenario exists: when the audio player is in different states such as loading resources, playing audio, finished playing, etc., it performs different actions (such as updating UI state) through some mechanism. That is, by listening to some objects and triggering different events when their state changes.

The above scenario can be implemented through EventEmitter, which can correspond to at least two patterns: Observer pattern and Publish/Subscribe pattern. Let’s understand these two patterns.

Observer Pattern

The Observer Pattern defines a 1:n relationship so that n observer objects listen to some observed object (subject object), and when the state of the observed object changes, it will actively notify all observer objects of this message, and different observer objects will produce different actions.

This behavioral pattern is concerned with the communication between the observer and the observed, and the publish/subscribe pattern is derived from it.

sobyte

Code Implementation

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
class Subject {
  constructor() {
    this.observers = [];
  }

  add(observer) {
    this.observers.push(observer);
  }

  notify(...args) {
    this.observers.forEach((observer) => observer.update(...args));
  }
}

class Observer {
  update(...args) {
    console.log(...args);
  }
}

Usage

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
// 创建 2 个观察者 observer1、observer2
const observer1 = new Observer();
const observer2 = new Observer();

// 创建 1 个目标 subject
const subject = new Subject();

// 目标 subject 主动添加 2 个观察者,建立 subject 与 observer1、observer2 依赖关系
subject.add(observer1);
subject.add(observer2);

doSomething();
// 目标触发某些事件、主动通知观察者
subject.notify("subject fired a event");

Since the observer pattern has no dispatching center, observers must add themselves to the observed for management; the observed will actively notify each observer when it triggers an event.

Publish/Subscribe Pattern

The main difference between the Pub/Sub Pattern and the Observer Pattern is that the former has an event dispatching center that filters all messages published by the publisher and distributes them to the subscribers, without the publisher and the subscriber caring whether the other exists or not.

sobyte

Code Implementation

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
class PubSub {
  constructor() {
    this.subscribers = [];
  }

  subscribe(topic, callback) {
    const callbacks = this.subscribers[topic];
    if (callbacks) {
      callbacks.push(callback);
    } else {
      this.subscribers[topic] = [callback];
    }
  }

  publish(topic, ...args) {
    const callbacks = this.subscribers[topic] || [];
    callbacks.forEach((callback) => callback(...args));
  }
}

Usage

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
// 创建事件调度中心,为发布者与订阅者提供调度服务
const pubSub = new PubSub();

// A 系统订阅了 SMS 事件,不关心谁将发布这个事件
pubSub.subscribe("lovelyEvent", console.log);
// B 系统订阅了 SMS 事件,不关心谁将发布这个事件
pubSub.subscribe("lovelyEvent", console.log);

// C 系统发布了 SMS 事件,不关心谁会订阅这个事件
pubSub.publish("lovelyEvent", "I just published a lovely event");

Compared with the two patterns, the publish/subscribe pattern facilitates decoupling between systems and is suitable for handling cross-system message events.

EventEmitter

EventEmitter is a class in the Node.js events module, used for unified event management in Node.js. By looking at its source implementation, we can try to construct our own EventEmitter to meet the scenario at the top of the article, as reflected in the following implementation of the publish/subscribe pattern.

The EventEmitter instance has the following main methods.

sobyte

Code Implementation

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
class EventEmitter {
  constructor() {
    this._events = Object.create(null);
  }

  /**
   * 为 EventEmitter 实例添加事件
   */
  addListener(eventName, listener) {
    if (this._events[eventName]) {
      this._events[eventName].push(listener);
    } else {
      this._events[eventName] = [listener];
    }
    return this;
  }

  /**
   * 移除某个事件
   * 调用该方法时,仅移除一个 Listener,参考下方 Case#7
   */
  removeListener(eventName, listener) {
    if (this._events[eventName]) {
      for (let i = 0; i < this._events[eventName].length; i++) {
        if (this._events[eventName][i] === listener) {
          this._events[eventName].splice(i, 1);

          // NOTE: 或调用 spliceOne 方法,参见文末
          // spliceOne(this._events[eventName], i);

          break;
        }
      }
    }
    return this;
  }

  /**
   * 移除指定事件名中的事件或全部事件
   */
  removeAllListeners(eventName) {
    if (this._events[eventName]) {
      this._events[eventName] = [];
    } else {
      this._events = Object.create(null);
    }
  }

  /**
   * 是 addListener 方法别名
   */
  on(eventName, listener) {
    return this.addListener(eventName, listener);
  }

  /**
   * 类似 on 方法,但通过 once 方法注册的事件被多次调用时只执行一次
   */
  once(eventName, listener) {
    let fired = false;
    let onceWrapperListener = (...args) => {
      this.off(eventName, onceWrapperListener);
      if (!fired) {
        fired = true;
        listener.apply(this, args);
      }
    };
    return this.on(eventName, onceWrapperListener);
  }

  /**
   * 是 removeListener 方法别名
   */
  off(eventName, listener) {
    return this.removeListener(eventName, listener);
  }

  /**
   * 主动触发事件
   */
  emit(eventName, ...args) {
    if (this._events[eventName]) {
      for (let i = 0; i < this._events[eventName].length; i++) {
        this._events[eventName][i].apply(this, args);
      }
    }
    return this;
  }
}

Usage

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
const emitter = new EventEmitter();

const fn1 = (name) => console.log(`fn1 ${name}`);
const fn2 = (name) => console.log(`fn2 ${name}`);

/**
 * Case 1
 */
emitter.on("event1", fn1);
emitter.on("event1", fn2);

emitter.emit("event1", "ju");
// Expected Output:
// fn1 ju
// fn2 ju

/**
 * Case 2
 */
emitter.once("event1", fn1);

emitter.emit("event1", "ju");
emitter.emit("event1", "ju");
emitter.emit("event1", "ju");

// Expected Output:
// fn1 ju

/**
 * Case 3
 */
emitter.on("event1", fn1);
emitter.on("event1", fn1);
emitter.off("event1", fn1);
emitter.on("event1", fn1);

emitter.emit("event1", "ju");

// Expected Output:
// fn1 ju
// fn1 ju

/**
 * Case 4
 */
emitter.on("event1", fn1);
emitter.on("event1", fn2);
emitter.on("event2", fn2);
// emitter.removeAllListeners('event1');

emitter.emit("event1", "ju");
emitter.emit("event2", "zhiyuan");

// Expected Output:
// fn1 ju
// fn2 ju
// fn2 zhiyuan

/**
 * Case 5
 */
emitter.on("event1", fn1);
emitter.on("event1", fn2);
emitter.on("event2", fn2);
emitter.removeAllListeners("event1");

emitter.emit("event1", "ju");
emitter.emit("event2", "zhiyuan");

// Expected Output:
// fn2 zhiyuan

/**
 * Case 6
 */
emitter.on("event1", fn1);
emitter.on("event1", fn2);
emitter.on("event2", fn2);
emitter.removeAllListeners();

emitter.emit("event1", "ju");
emitter.emit("event2", "zhiyuan");

// Expected Output:
// (empty output)

/**
 * Case 7
 */
emitter.on("event1", fn1);
emitter.on("event1", fn1);
emitter.off("event1", fn1);
emitter.on("event1", fn2);

emitter.emit("event1", "ju");
// Expected Output:
// fn1 ju
// fn2 ju

Note that when calling the off method, the Listener function passed in should be the same as the Listener function passed in the on method, do not pass in an anonymous function, otherwise the off method will be invalid. This is because in JavaScript, (function) objects are passed by reference, and two functions with the same logic are not equal. Therefore, we define the function first and then pass the function to the on/off method.

Also, when an instance fires an event, all the Listener associated with it will be called simultaneously (see the emit method implementation: a circular array, called in sequence).

Other

spliceOne method

In the official events implementation, when calling the removeListener method to remove a Listener function, a custom spliceOne method is called, which is more efficient than splice.

The implementation is as follows.

1
2
3
4
5
6
7
function spliceOne(list, index) {
  for (; index + 1 < list.length; index++) {
    list[index] = list[index + 1];
  }

  list.pop();
}

return this

return this returns the current instance of the object. In the EventEmitter implementation of this article, this refers to the emitter, which allows for chain calls.

1
2
3
4
5
6
7
const emitter = new EventEmitter();

const fn = () => console.log("Hello");

emiter.on("event", fn).emit("event");
// or
emiter.once("event", fn).emit("event");