fix: avoid creating functions on each handleEvent() call

This commit is contained in:
shirakaba
2022-12-18 01:45:04 +09:00
parent 66f62fbbd6
commit f08d122add

View File

@@ -231,6 +231,26 @@ export class DOMEvent implements Event {
this.propagationState = EventPropagationState.stop; this.propagationState = EventPropagationState.stop;
} }
// During handleEvent(), we want to work on a copy of the listeners array,
// as any callback could modify the original array during the loop.
//
// However, cloning arrays is expensive on this hot path, so we'll do it
// lazily - i.e. only take a clone if a mutation is about to happen.
// This optimisation is particularly worth doing as it's very rare that
// an event listener callback will end up modifying the listeners array.
private listenersLive: MutationSensitiveArray<ListenerEntry> = emptyArray;
private listenersLazyCopy: ListenerEntry[] = emptyArray;
// Creating this upon class construction as an arrow function rather than as
// an inline function bound afresh on each usage saves about 210 nanoseconds
// per run of handleEvent().
private onCurrentListenersMutation = () => {
// Cloning the array via spread syntax is up to 180 nanoseconds
// faster per run than using Array.prototype.slice().
this.listenersLazyCopy = [...this.listenersLive];
this.listenersLive.onMutation = null;
};
/** /**
* Dispatches a synthetic event event to target and returns true if either * Dispatches a synthetic event event to target and returns true if either
* event's cancelable attribute value is false or its preventDefault() * event's cancelable attribute value is false or its preventDefault()
@@ -257,6 +277,8 @@ export class DOMEvent implements Event {
this.target = null; this.target = null;
this.eventPhase = this.NONE; this.eventPhase = this.NONE;
this.propagationState = EventPropagationState.resume; this.propagationState = EventPropagationState.resume;
this.listenersLive = emptyArray;
this.listenersLazyCopy = emptyArray;
}; };
// `Observable.removeEventListener` would likely suffice, but grabbing // `Observable.removeEventListener` would likely suffice, but grabbing
@@ -282,10 +304,10 @@ export class DOMEvent implements Event {
// event. This keeps behaviour as consistent with DOM Events as // event. This keeps behaviour as consistent with DOM Events as
// possible. // possible.
this.listenersLazyCopy = this.listenersLive = getGlobalEventHandlersPreHandling?.() || emptyArray;
this.handleEvent({ this.handleEvent({
data, data,
isGlobal: true, isGlobal: true,
getListenersForType: () => getGlobalEventHandlersPreHandling?.() ?? emptyArray,
removeEventListener: removeGlobalEventListener, removeEventListener: removeGlobalEventListener,
phase: this.CAPTURING_PHASE, phase: this.CAPTURING_PHASE,
}); });
@@ -297,13 +319,14 @@ export class DOMEvent implements Event {
this.currentTarget = currentTarget; this.currentTarget = currentTarget;
this.eventPhase = this.target === this.currentTarget ? this.AT_TARGET : this.CAPTURING_PHASE; this.eventPhase = this.target === this.currentTarget ? this.AT_TARGET : this.CAPTURING_PHASE;
this.listenersLazyCopy = this.listenersLive = currentTarget.getEventList(this.type) || emptyArray;
this.handleEvent({ this.handleEvent({
data, data,
isGlobal: false, isGlobal: false,
getListenersForType: () => currentTarget.getEventList(this.type) ?? emptyArray,
removeEventListener: currentTarget.removeEventListener.bind(currentTarget) as Observable['removeEventListener'], removeEventListener: currentTarget.removeEventListener.bind(currentTarget) as Observable['removeEventListener'],
phase: this.CAPTURING_PHASE, phase: this.CAPTURING_PHASE,
}); });
if (this.propagationState !== EventPropagationState.resume) { if (this.propagationState !== EventPropagationState.resume) {
reset(); reset();
return this.returnValue; return this.returnValue;
@@ -316,13 +339,14 @@ export class DOMEvent implements Event {
const currentTarget = eventPath[i]; const currentTarget = eventPath[i];
this.eventPhase = this.target === this.currentTarget ? this.AT_TARGET : this.BUBBLING_PHASE; this.eventPhase = this.target === this.currentTarget ? this.AT_TARGET : this.BUBBLING_PHASE;
this.listenersLazyCopy = this.listenersLive = currentTarget.getEventList(this.type) || emptyArray;
this.handleEvent({ this.handleEvent({
data, data,
isGlobal: false, isGlobal: false,
getListenersForType: () => currentTarget.getEventList(this.type) ?? emptyArray,
removeEventListener: currentTarget.removeEventListener.bind(currentTarget) as Observable['removeEventListener'], removeEventListener: currentTarget.removeEventListener.bind(currentTarget) as Observable['removeEventListener'],
phase: this.BUBBLING_PHASE, phase: this.BUBBLING_PHASE,
}); });
if (this.propagationState !== EventPropagationState.resume) { if (this.propagationState !== EventPropagationState.resume) {
reset(); reset();
return this.returnValue; return this.returnValue;
@@ -341,10 +365,10 @@ export class DOMEvent implements Event {
this.eventPhase = this.BUBBLING_PHASE; this.eventPhase = this.BUBBLING_PHASE;
} }
this.listenersLazyCopy = this.listenersLive = getGlobalEventHandlersPostHandling?.() || emptyArray;
this.handleEvent({ this.handleEvent({
data, data,
isGlobal: true, isGlobal: true,
getListenersForType: () => getGlobalEventHandlersPostHandling?.() ?? emptyArray,
removeEventListener: removeGlobalEventListener, removeEventListener: removeGlobalEventListener,
phase: this.BUBBLING_PHASE, phase: this.BUBBLING_PHASE,
}); });
@@ -353,32 +377,12 @@ export class DOMEvent implements Event {
return this.returnValue; return this.returnValue;
} }
private handleEvent({ data, isGlobal, getListenersForType, phase, removeEventListener }: { data: EventData; isGlobal: boolean; getListenersForType: () => MutationSensitiveArray<ListenerEntry>; phase: 0 | 1 | 2 | 3; removeEventListener: (eventName: string, callback?: any, thisArg?: any, capture?: boolean) => void }) { private handleEvent({ data, isGlobal, phase, removeEventListener }: { data: EventData; isGlobal: boolean; phase: 0 | 1 | 2 | 3; removeEventListener: (eventName: string, callback?: any, thisArg?: any, capture?: boolean) => void }) {
// We want to work on a copy of the array, as any callback could modify
// the original array during the loop.
//
// However, cloning arrays is expensive on this hot path, so we'll do it
// lazily - i.e. only take a clone if a mutation is about to happen.
// This optimisation is particularly worth doing as it's very rare that
// an event listener callback will end up modifying the listeners array.
const listenersLive: MutationSensitiveArray<ListenerEntry> = getListenersForType();
// Set a listener to clone the array just before any mutations. // Set a listener to clone the array just before any mutations.
let listenersLazyCopy: ListenerEntry[] = listenersLive; this.listenersLive.onMutation = this.onCurrentListenersMutation;
listenersLive.onMutation = () => (mutation: string, payload?: unknown) => {
console.log(`handleEvent "${data.eventName}": doLazyCopy due to "${mutation}"`, payload);
// Cloning the array via spread syntax is up to 180 nanoseconds
// faster per run than using Array.prototype.slice().
listenersLazyCopy = [...listenersLive];
listenersLive.onMutation = null;
};
// Make sure we clear the callback before we exit the function, for (let i = this.listenersLazyCopy.length - 1; i >= 0; i--) {
// otherwise we may wastefully clone the array on future mutations. const listener = this.listenersLazyCopy[i];
const cleanup = () => (listenersLive.onMutation = null);
for (let i = listenersLazyCopy.length - 1; i >= 0; i--) {
const listener = listenersLazyCopy[i];
// Assigning variables this old-fashioned way is up to 50 // Assigning variables this old-fashioned way is up to 50
// nanoseconds faster per run than ESM destructuring syntax. // nanoseconds faster per run than ESM destructuring syntax.
@@ -394,7 +398,7 @@ export class DOMEvent implements Event {
// We simply use a strict equality check here because we trust that // We simply use a strict equality check here because we trust that
// the listeners provider will never allow two deeply-equal // the listeners provider will never allow two deeply-equal
// listeners into the array. // listeners into the array.
if (!listenersLive.includes(listener)) { if (!this.listenersLive.includes(listener)) {
continue; continue;
} }
@@ -424,12 +428,13 @@ export class DOMEvent implements Event {
} }
if (this.propagationState === EventPropagationState.stopImmediate) { if (this.propagationState === EventPropagationState.stopImmediate) {
cleanup(); break;
return;
} }
} }
cleanup(); // Make sure we clear the callback before we exit the function,
// otherwise we may wastefully clone the array on future mutations.
this.listenersLive.onMutation = null;
} }
} }