function LoopObservable() {
const listeners = new Set();
this.on = fn => listeners.add(fn);
this.off = fn => listeners.delete(fn);
this.clear = () => listeners.clear();
// This has bad failure modes.
this.send = function(message) {
for (let fn of listeners)
fn(message);
};
};
function EventTargetObservable() {
// Name is arbitrary because we're using a dedicated object. Use a shorter
// name for shorter lookup.
const NAME = '.';
const __ = document.createTextNode(null);
this.on = function(handler) {
__.addEventListener(NAME, event => {
handler(event.detail);
});
};
this.off = function(handler) {
__.removeEventListener(NAME, handler);
};
this.send = function(detail) {
__.dispatchEvent(new CustomEvent(NAME, {detail}));
};
}
class ExtendedEvent extends Event {
constructor(name, data){
super(name);
this.D = data;
}
}
function ExtendedEventTargetObservable() {
// Name is arbitrary because we're using a dedicated object. Use a shorter
// name for shorter lookup.
const NAME = '.';
const __ = document.createTextNode(null);
this.on = function(handler) {
__.addEventListener(NAME, event => {
handler(event.D);
});
};
this.off = function(handler) {
__.removeEventListener(NAME, handler);
};
this.send = function(data) {
__.dispatchEvent(new ExtendedEvent(NAME, data));
};
}
function run_test(observable, handlers = 2, calls = 1000) {
for (let i = 0; i < handlers; i++)
observable.on(message => console.log(i, message));
for (let i = 0; i < calls; i++)
observable.send({type: "hello", i});
}
run_test(new LoopObservable());
run_test(new EventTargetObservable());
run_test(new ExtendedEventTargetObservable());
--enable-precise-memory-info
flag.
Test case name | Result |
---|---|
loop observable | |
EventTarget observable | |
ExtendedEvent observable |
Test name | Executions per second |
---|---|
loop observable | 175.9 Ops/sec |
EventTarget observable | 165.0 Ops/sec |
ExtendedEvent observable | 177.0 Ops/sec |
Let's break down the provided benchmark and its components.
Benchmark Overview
The benchmark, titled "Observables: loops versus EventTarget vs extended event", compares three approaches for handling observers in JavaScript:
EventTarget
APIEventTarget
, which wraps the original API to provide a more robust failure mode.Loop Approach
The loop approach uses a Set
to store callback functions, allowing them to be added, removed, and cleared dynamically. When an event is sent, each callback in the set is invoked with the provided message. This approach has two significant drawbacks:
EventTarget Approach
The native EventTarget
API provides a more robust failure mode than the loop approach. When an event is dispatched, each listener is executed in its own execution context, providing better isolation between listeners. However:
CustomEvent
instance may introduce overhead compared to the native loop approach.Extended EventTarget Approach
The extended version of EventTarget
, ExtendedEventTargetObservable
, wraps the original API to provide a more robust failure mode:
However, this approach also introduces additional overhead compared to the native EventTarget
API.
Options Compared
The benchmark compares three options:
EventTarget
API (better isolation, but possible overhead)EventTarget
API (robust failure modes, explicit error handling, but additional overhead)Pros and Cons of Each Approach
Approach | Pros | Cons |
---|---|---|
Loops with callbacks | Easy to implement, simple | Bad failure modes, silently swallowing errors |
Native EventTarget API |
Better isolation, robust | Possible overhead, lookup by name |
Extended EventTarget API |
Robust failure modes, explicit error handling | Additional overhead |
Other Considerations
Alternative Approaches
While not explicitly mentioned in the benchmark, other approaches for handling observers in JavaScript include:
These alternatives may offer better performance, scalability, and maintainability compared to the approaches presented in the benchmark.