mirror of
https://github.com/NativeScript/NativeScript.git
synced 2025-11-05 13:26:48 +08:00
fix: avoid creating functions on each handleEvent() call
This commit is contained in:
@@ -231,6 +231,26 @@ export class DOMEvent implements Event {
|
||||
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
|
||||
* event's cancelable attribute value is false or its preventDefault()
|
||||
@@ -257,6 +277,8 @@ export class DOMEvent implements Event {
|
||||
this.target = null;
|
||||
this.eventPhase = this.NONE;
|
||||
this.propagationState = EventPropagationState.resume;
|
||||
this.listenersLive = emptyArray;
|
||||
this.listenersLazyCopy = emptyArray;
|
||||
};
|
||||
|
||||
// `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
|
||||
// possible.
|
||||
|
||||
this.listenersLazyCopy = this.listenersLive = getGlobalEventHandlersPreHandling?.() || emptyArray;
|
||||
this.handleEvent({
|
||||
data,
|
||||
isGlobal: true,
|
||||
getListenersForType: () => getGlobalEventHandlersPreHandling?.() ?? emptyArray,
|
||||
removeEventListener: removeGlobalEventListener,
|
||||
phase: this.CAPTURING_PHASE,
|
||||
});
|
||||
@@ -297,13 +319,14 @@ export class DOMEvent implements Event {
|
||||
this.currentTarget = currentTarget;
|
||||
this.eventPhase = this.target === this.currentTarget ? this.AT_TARGET : this.CAPTURING_PHASE;
|
||||
|
||||
this.listenersLazyCopy = this.listenersLive = currentTarget.getEventList(this.type) || emptyArray;
|
||||
this.handleEvent({
|
||||
data,
|
||||
isGlobal: false,
|
||||
getListenersForType: () => currentTarget.getEventList(this.type) ?? emptyArray,
|
||||
removeEventListener: currentTarget.removeEventListener.bind(currentTarget) as Observable['removeEventListener'],
|
||||
phase: this.CAPTURING_PHASE,
|
||||
});
|
||||
|
||||
if (this.propagationState !== EventPropagationState.resume) {
|
||||
reset();
|
||||
return this.returnValue;
|
||||
@@ -316,13 +339,14 @@ export class DOMEvent implements Event {
|
||||
const currentTarget = eventPath[i];
|
||||
this.eventPhase = this.target === this.currentTarget ? this.AT_TARGET : this.BUBBLING_PHASE;
|
||||
|
||||
this.listenersLazyCopy = this.listenersLive = currentTarget.getEventList(this.type) || emptyArray;
|
||||
this.handleEvent({
|
||||
data,
|
||||
isGlobal: false,
|
||||
getListenersForType: () => currentTarget.getEventList(this.type) ?? emptyArray,
|
||||
removeEventListener: currentTarget.removeEventListener.bind(currentTarget) as Observable['removeEventListener'],
|
||||
phase: this.BUBBLING_PHASE,
|
||||
});
|
||||
|
||||
if (this.propagationState !== EventPropagationState.resume) {
|
||||
reset();
|
||||
return this.returnValue;
|
||||
@@ -341,10 +365,10 @@ export class DOMEvent implements Event {
|
||||
this.eventPhase = this.BUBBLING_PHASE;
|
||||
}
|
||||
|
||||
this.listenersLazyCopy = this.listenersLive = getGlobalEventHandlersPostHandling?.() || emptyArray;
|
||||
this.handleEvent({
|
||||
data,
|
||||
isGlobal: true,
|
||||
getListenersForType: () => getGlobalEventHandlersPostHandling?.() ?? emptyArray,
|
||||
removeEventListener: removeGlobalEventListener,
|
||||
phase: this.BUBBLING_PHASE,
|
||||
});
|
||||
@@ -353,32 +377,12 @@ export class DOMEvent implements Event {
|
||||
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 }) {
|
||||
// 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();
|
||||
|
||||
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 }) {
|
||||
// Set a listener to clone the array just before any mutations.
|
||||
let listenersLazyCopy: ListenerEntry[] = listenersLive;
|
||||
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;
|
||||
};
|
||||
this.listenersLive.onMutation = this.onCurrentListenersMutation;
|
||||
|
||||
// Make sure we clear the callback before we exit the function,
|
||||
// otherwise we may wastefully clone the array on future mutations.
|
||||
const cleanup = () => (listenersLive.onMutation = null);
|
||||
|
||||
for (let i = listenersLazyCopy.length - 1; i >= 0; i--) {
|
||||
const listener = listenersLazyCopy[i];
|
||||
for (let i = this.listenersLazyCopy.length - 1; i >= 0; i--) {
|
||||
const listener = this.listenersLazyCopy[i];
|
||||
|
||||
// Assigning variables this old-fashioned way is up to 50
|
||||
// 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
|
||||
// the listeners provider will never allow two deeply-equal
|
||||
// listeners into the array.
|
||||
if (!listenersLive.includes(listener)) {
|
||||
if (!this.listenersLive.includes(listener)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -424,12 +428,13 @@ export class DOMEvent implements Event {
|
||||
}
|
||||
|
||||
if (this.propagationState === EventPropagationState.stopImmediate) {
|
||||
cleanup();
|
||||
return;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user