Publish-subscribe design pattern often involve in the program, for example Vue $$off on and, in the document. The addEventListener (), the document. The removeEventListener (), etc., issued a subscription model can reduce the coupling of the program, Unified management of maintenance messages and event handling also makes the program easier to maintain and extend.
Some friends ask me how to learn design patterns. Design patterns themselves are abstract solutions to some problems and scenarios. It is no good to memorize them by rote, which is no different from building castles in the air.
The eventEmitter3 library is used to manage communication between components. You can learn how to use the library with a simple Readme document, but it is also useful to understand the publish and subscribe design pattern.
A, definitions,
In software architecture, publish subscribe is a message paradigm in which a sender of a message (called a publisher) does not directly send a message to a specific recipient (called a subscriber), but rather divides published messages into different categories without knowing which subscribers (if any) may exist. Similarly, subscribers can express interest in one or more categories and receive only messages of interest without knowing which publishers (if any) exist.
An analogy is well understood, such as wechat public account. If you follow (understand as subscription) the “DYBOY” public account, when the public account published a new article, wechat will notify you, but not other subscribers of the public account. In addition, you can subscribe to multiple public accounts.
In the components of the program, the communication of multiple components in addition to the father-child component value transmission, as well as such as REdux, VUEX state management, and the publish and subscribe mode mentioned in this paper, can be realized through an event center.
Second, hand rub a publish/subscribe event center
So by definition, let’s try to implement a JavaScript version of the publish/subscribe event center and see what the problems are.
2.1 Basic Structure edition
DiyEventEmitter was first implemented as follows:
/** * Event publishing and Subscription center */
class DiyEventEmitter {
static instance: DiyEventEmitter;
private _eventsMap: Map<string.Array<() = > void> >;static getInstance() {
if(! DiyEventEmitter.instance) { DiyEventEmitter.instance =new DiyEventEmitter();
}
return DiyEventEmitter.instance;
}
constructor() {
this._eventsMap = new Map(a);// Map the event name to the callback function
}
/** ** Event subscription **@param EventName eventName *@param EventFnCallback The callback function */ when the event occurs
public on(eventName: string, eventFnCallback: () => void) {
const newArr = this._eventsMap.get(eventName) || [];
newArr.push(eventFnCallback);
this._eventsMap.set(eventName, newArr);
}
/** * unsubscribe **@param EventName eventName *@param EventFnCallback The callback function */ when the event occurs
public off(eventName: string, eventFnCallback? : () = >void) {
if(! eventFnCallback) {this._eventsMap.delete(eventName);
return;
}
const newArr = this._eventsMap.get(eventName) || [];
for (let i = newArr.length - 1; i >= 0; i--) {
if (newArr[i] === eventFnCallback) {
newArr.splice(i, 1); }}this._eventsMap.set(eventName, newArr);
}
/** * Actively notifies and executes the registered callback function **@param EventName eventName */
public emit(eventName: string) {
const fns = this._eventsMap.get(eventName) || [];
fns.forEach(fn= >fn()); }}export default DiyEventEmitter.getInstance();
Copy the code
The derived DiyEventEmitter is a “singleton” that ensures that there is only one instance of the “event center” globally, and the public method can be used directly
import e from "./DiyEventEmitter";
const subscribeFn = () = > {
console.log("DYBOY subscription received a message.");
};
const subscribeFn2 = () = > {
console.log("DYBOY's second subscription received a message.");
};
/ / subscribe
e.on("dyboy", subscribeFn);
e.on("dyboy", subscribeFn2);
// Publish the message
e.emit("dyboy");
Unbind the first subscription message
e.off("dyboy", subscribeFn);
// Second release
e.emit("dyboy");
Copy the code
Output console result:
DYBOY subscription received a message for the second subscription for the second subscriptionCopy the code
The first release’s publish subscription Event center, which supports subscribe, publish, and cancel, is OK.
2.2 Support to subscribe to once method only
In some scenarios, certain event subscriptions may only need to be executed once and subsequent notifications will not be responded to.
Implementation idea: The new once subscription method, when the response to the corresponding “publisher message”, the active unsubscription of the current callback function.
Add a new type to make it easier to extend the description of the callback function:
type SingleEvent = {
fn: () = > void;
once: boolean;
};
Copy the code
The type of _eventsMap changed to:
private _eventsMap: Map<string.Array<SingleEvent>>;
Copy the code
The public addListener method is shared by the ON and once methods:
private addListener( eventName: string, eventFnCallback: () => void, once = false) {
const newArr = this._eventsMap.get(eventName) || [];
newArr.push({
fn: eventFnCallback,
once,
});
this._eventsMap.set(eventName, newArr);
}
/** ** Event subscription **@param EventName eventName *@param EventFnCallback The callback function */ when the event occurs
public on(eventName: string, eventFnCallback: () => void) {
this.addListener(eventName, eventFnCallback);
}
/** * events subscribe once **@param EventName eventName *@param EventFnCallback The callback function */ when the event occurs
public once(eventName: string, eventFnCallback: () => void) {
this.addListener(eventName, eventFnCallback, true);
}
Copy the code
At the same time, we need to consider unsubscribing once the event is triggered
/** * trigger: actively notifies and executes the registered callback function **@param EventName eventName */
public emit(eventName: string) {
const fns = this._eventsMap.get(eventName) || [];
fns.forEach((evt, index) = > {
evt.fn();
if (evt.once) fns.splice(index, 1);
});
this._eventsMap.set(eventName, fns);
}
Copy the code
In addition, the comparison in the unsubscribe function needs to replace the object attribute comparison: newArr[I].fn === eventFnCallback
This completes the transformation of our event center to support the once method.
2.3 Caching Published Messages
Under framework development, components are typically loaded asynchronously on demand, and if the publisher component publishes a message first, but the asynchronous component has not finished loading (subscription registration is completed), the publisher’s publish message will not be responded to. Therefore, we need to queue messages as a cache until a subscriber subscrires and responds to a cached publication message only once, and the message is dequeued from the cache.
First, let’s review the logical flow of caching messages:
Center detects whether there is any subscriber publishers publish news, events, if there are no subscribers subscribe to this message, then put the message buffer to offline message queue, when a subscriber subscription, detect whether to subscribe to the event messages in the cache, if it is, then the cache of news events, in turn, the team (FCFS scheduling execution), triggering the subscriber performs a callback function.
New offline message cache queue:
private _offlineMessageQueue: Map<string.number>;
Copy the code
Determine if the corresponding event has a subscriber in the EMIT publish message, and update to the offline event message if there is no subscriber
** @param eventName */ public emit(eventName: string) { const fns = this._eventsMap.get(eventName) || [];+ if (fns.length === 0) {
+ const counter = this._offlineMessageQueue.get(eventName) || 0;
+ this._offlineMessageQueue.set(eventName, counter + 1);
+ return;
+}
fns.forEach((evt, index) => {
evt.fn();
if (evt.once) fns.splice(index, 1);
});
this._eventsMap.set(eventName, fns);
}
Copy the code
Then, according to the count of offline event messages in the addListener method, emit the event message again, trigger the message callback function to execute, and delete the corresponding event in the offline message.
private addListener(
eventName: string,
eventFnCallback: () => void,
once = false
) {
const newArr = this._eventsMap.get(eventName) || [];
newArr.push({
fn: eventFnCallback,
once,
});
this._eventsMap.set(eventName, newArr);
+ const cacheMessageCounter = this._offlineMessageQueue.get(eventName);
+ if (cacheMessageCounter) {
+ for (let i = 0; i < cacheMessageCounter; i++) {
+ this.emit(eventName);
+}
+ this._offlineMessageQueue.delete(eventName);
+}
}
Copy the code
So you have an event center that supports offline messaging!
2.4 Callback function data transmission parameter & execution environment
In the callback function of above, we can find that is a no return value, not into the function, this is some chicken ribs, at the time of function will point execution context, may contain some callback functions on this point cannot be bound to the event center, therefore in accordance with the need of the callback function binding execution context.
2.4.1 Data transmission parameters for callback function are supported
First, change the TypeScript Function type fn: () => void to fn: Function to pass TS validation of any length of Function arguments.
In fact, the callback function has no parameters in the event center, if any parameters are passed in through the parameter bind.
If you really want to support the callback function, you would need to emit() and then pass the argument to the callback function, which we won’t implement for now.
2.4.2 Binding an Environment
Before implementing execution environment binding, consider the following question: “Should the developer bind itself or should the event center do it?”
In other words, should developers actively bind this to on(‘eventName’, the callback function)? Under the current design, it is considered appropriate for callback functions without arguments to bind this themselves.
Therefore, there is no need to bind parameters in the event center. If there are callback functions that need to pass parameters or bind execution context, you need to bind them when binding the callback function. In this way, our event center also ensures the purity of the function.
Here we hand rub simple publish and subscribe event center to complete!
3. Learn the design and implementation of EventEmitter3
While we’ve implemented a version of EventEmitter3 as we understand it, we don’t know if it’s good or bad without a comparison, so let’s take a look at how EventEmitter3, an excellent “extreme performance optimization” library, handles event subscriptions and publishing, and learn some performance optimization ideas.
First, EventEmitter3 (later abbreviated: The Events object is used as the memory of the “callback event object”, which is similar to the “publish-subscribe mode” implemented above as the execution logic of the event. In addition, the addListener() function adds the parameters of the execution context, and the emit() function supports up to 5 parameters. Listener counts and event name prefixes are also added to EventEmitter3.
3.1 Events Memory
To avoid translation, and to improve compatibility and performance, EventEmitter3 is written in ES5.
In JavaScript everything is an object and functions are objects, so the implementation of memory:
function Events() {}
Copy the code
3.2 Event listener instance
Similarly, we used the singleEvent object above to store each event listener instance, and EE3 used an EE object to store each event listener instance and the necessary properties
/** * The representation of each event listener instance **@param {Function} Fn listener function *@param {*} Context invokes the listener's execution context *@param {Boolean} [once=false] specifies whether the listener supports only one call to *@constructor
* @private* /
function EE(fn, context, once) {
this.fn = fn;
this.context = context;
this.once = once || false;
}
Copy the code
3.3 Adding listener methods
/** * adds listener ** for the given event@param {EventEmitter} A reference to an Emitter EventEmitter instance@param {(String|Symbol)} Event Indicates the event name. *@param {Function} Fn listener function. *@param {*} Context Calls the listener's context. *@param {Boolean} Once specifies whether the listener supports only one call. *@returns {EventEmitter}
* @private* /
function addListener(emitter, event, fn, context, once) {
if (typeoffn ! = ='function') {
throw new TypeError('The listener must be a function');
}
var listener = new EE(fn, context || emitter, once)
, evt = prefix ? prefix + event : event;
// TODO:Why use objects in the first place, and what's the benefit of using an array of objects when you have multiple objects?
if(! emitter._events[evt]) emitter._events[evt] = listener, emitter._eventsCount++;else if(! emitter._events[evt].fn) emitter._events[evt].push(listener);else emitter._events[evt] = [emitter._events[evt], listener];
return emitter;
}
Copy the code
This “Add listener” method has several key function points:
- If there is a prefix, add a prefix to the event name to avoid event conflicts
- The event name is added each time
_eventsCount+1
, used to quickly read and write the number of all events - If the event has a single listener
_events[evt]
Point to theEE
Object, more efficient access
3.4 Clearing Events
/** * Clear event ** by event name@param {EventEmitter} References to The Emitter EventEmitter instance@param {(String|Symbol)} Evt event name *@private* /
function clearEvent(emitter, evt) {
if (--emitter._eventsCount === 0) emitter._events = new Events();
else delete emitter._events[evt];
}
Copy the code
To clear the event, simply delete the property on the object using the delete keyword
Another neat trick here is that depending on the event counter, if the counter is zero, we create a new Events memory pointing to the _events property of the Emitter.
The advantage of this is that if you need to clear all events, you can simply assign 1 to emitters._eventsCount and call clearEvent() instead of iterating through the cleanup events
3.5 EventEmitter
function EventEmitter() {
this._events = new Events();
this._eventsCount = 0;
}
Copy the code
The EventEmitter object references the event triggers in NodeJS and defines a minimal interface model, including the _events and _eventsCount properties, with additional methods added through prototypes.
EventEmitter objects are equivalent to our definition of event centers above, and their functions are summarized as follows:
The emit() method is essential, and the on() and once() methods used by subscribers to register events are both addListener() utility functions.
The emit() method is implemented as follows:
/** * calls each listener ** that executes the specified event name@param {(String|Symbol)} Event Indicates the event name. *@returns {Boolean} 'true' returns false. * if the current event name has no listener bound@public* /
EventEmitter.prototype.emit = function emit(event, a1, a2, a3, a4, a5) {
var evt = prefix ? prefix + event : event;
if (!this._events[evt]) return false;
var listeners = this._events[evt]
, len = arguments.length
, args
, i;
// If only one listener is bound to the event name
if (listeners.fn) {
// Remove the listener if it is executed once
if (listeners.once) this.removeListener(event, listeners.fn, undefined.true);
/ / Refrence: https://juejin.cn/post/6844903496450310157
// This is handled for performance reasons by calling the call method with five incoming arguments
// Use apply for more than 5 parameters
// Most scenarios with more than 5 parameters are rare
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 {
// When there are multiple listeners bound to the same event name
var length = listeners.length
, j;
// Loop through each bound event listener
for (i = 0; i < length; i++) {
if (listeners[i].once) this.removeListener(event, listeners[i].fn, undefined.true);
switch (len) {
case 1: listeners[i].fn.call(listeners[i].context); break;
case 2: listeners[i].fn.call(listeners[i].context, a1); break;
case 3: listeners[i].fn.call(listeners[i].context, a1, a2); break;
case 4: listeners[i].fn.call(listeners[i].context, a1, a2, a3); break;
default:
if(! args)for (j = 1, args = new Array(len -1); j < len; j++) {
args[j - 1] = arguments[j]; } listeners[i].fn.apply(listeners[i].context, args); }}}return true;
};
Copy the code
The emit() method displays five incoming arguments, A1 to a5, with the call() method binding this to point to and execute the listener’s callback in preference.
The reason for this is that the Call method is more efficient than the Apply method. Please refer to the Call and Apply Performance Comparison for verification discussion.
Basically the implementation of EventEmitter3 is done!
Four,
EventEmitter3 is a tool library that claims to be the ultimate in event publishing and subscription optimization.
- The difference in efficiency between Call and apply
- Object and object array access performance considerations
- Understand the publish-subscribe model and its application in event systems