Skip to content

Instantly share code, notes, and snippets.

@AStaroverov
Created April 27, 2018 08:54
Show Gist options
  • Save AStaroverov/a7f00829a5e2a1186a28b0f49dfaa51f to your computer and use it in GitHub Desktop.
Save AStaroverov/a7f00829a5e2a1186a28b0f49dfaa51f to your computer and use it in GitHub Desktop.
EventEmitter with fastest off
const TIME_PER_FRAME_FOR_GC = 5; // 5 mc
const rIC = window.requestIdleCallback || window.setTimeout;
const noop = () => {};
const getTime = () => window.performance.now();
export class Emitter {
constructor () {
this._destroyed = false;
this._eventsForGC = new Set();
this._mapEventToFnWrapper = new Map();
this._mapEventToMapFnToFnWrapper = new Map();
}
on (event, fn, context) {
this._on(event, fn, context, false);
}
once (event, fn, context) {
this._on(event, fn, context, true);
}
off (event, fn) {
if (this._destroyed) return;
if (this._mapEventToMapFnToFnWrapper.has(event)) {
if (typeof fn === 'function' && this._mapEventToMapFnToFnWrapper.get(event).has(fn)) {
this._mapEventToMapFnToFnWrapper.get(event).get(fn).destroy();
} else {
const fnWrappers = this._mapEventToFnWrapper.get(event);
for (let i = 0; i < fnWrappers.length; i += 1) {
fnWrappers[i].destroy();
}
}
this._eventsForGC.add(event);
this._launchGC(event)
}
}
emit (event, a1, a2, a3, a4, a5) {
if (this._destroyed) return;
if (!this._mapEventToFnWrapper.has(event)) return;
const len = arguments.length;
const fnWrappers = this._mapEventToFnWrapper.get(event);
for (let i = 0; i < fnWrappers.length; i += 1) {
switch (len) {
case 1: fnWrappers[i].run(); break;
case 2: fnWrappers[i].run(a1); break;
case 3: fnWrappers[i].run(a1, a2); break;
case 4: fnWrappers[i].run(a1, a2, a3); break;
case 5: fnWrappers[i].run(a1, a2, a3, a4); break;
case 6: fnWrappers[i].run(a1, a2, a3, a4, a5); break;
default: {
for (let i = 1, args = new Array(len - 1); i < len; i++) {
args[i - 1] = arguments[i];
}
fnWrappers[i].run.apply(fnWrappers[i], args)
}
}
}
}
destroy () {
if (this._destroyed) return;
this._destroyed = true;
this._eventsForGC = undefined;
this._mapEventToFnWrapper = undefined;
this._mapEventToMapFnToFnWrapper = undefined;
}
_on (event, fn, context, once) {
if (this._destroyed) return;
if (!this._mapEventToFnWrapper.has(event)) {
this._mapEventToFnWrapper.set(event, []);
this._mapEventToMapFnToFnWrapper.set(event, new WeakMap());
}
const fnWrapper = new FnWrapper(fn, context || null, Boolean(once));
this._mapEventToFnWrapper.get(event).push(fnWrapper);
this._mapEventToMapFnToFnWrapper.get(event).set(fn, fnWrapper);
}
_launchGC () {
if (this._gcLaunched || this._destroyed) return;
this._gcLaunched = true;
rIC(() => {
this._gcLaunched = false;
this._walkGC();
});
}
_walkGC () {
if (this._destroyed) return;
const startTime = getTime();
for (const event of this._eventsForGC) {
const newFnWrappers = [];
const fnWrappers = this._mapEventToFnWrapper.get(event);
for (let i = 0; i < fnWrappers.length; i += 1) {
if (!fnWrappers[i].canBeDeleted) {
newFnWrappers.push(fnWrappers[i]);
}
}
this._mapEventToFnWrapper.set(event, newFnWrappers);
this._eventsForGC.delete(event);
if (getTime() - startTime >= TIME_PER_FRAME_FOR_GC) {
this._launchGC();
return;
}
}
}
}
class FnWrapper {
constructor (fn, conctext, once) {
this.fn = fn;
this.once = once;
this.context = conctext;
this.canBeDeleted = false;
}
run (a1, a2, a3, a4, a5) {
switch (arguments.length) {
case 0: this.fn.call(this.context); break;
case 1: this.fn.call(this.context, a1); break;
case 2: this.fn.call(this.context, a1, a2); break;
case 3: this.fn.call(this.context, a1, a2, a3); break;
case 4: this.fn.call(this.context, a1, a2, a3, a4); break;
case 5: this.fn.call(this.context, a1, a2, a3, a4, a5); break;
default: this.fn.apply(this.context, Array.form(arguments));
}
if (this.once) {
this.destroy();
}
}
destroy () {
this.fn = noop;
this.canBeDeleted = true;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment