mirror of
https://github.com/NativeScript/NativeScript.git
synced 2025-11-05 13:26:48 +08:00
fix: lazy-clone listeners array
This commit is contained in:
@@ -1,5 +1,6 @@
|
|||||||
import type { EventData, ListenerEntry, Observable } from '../observable/index';
|
import type { EventData, ListenerEntry, Observable } from '../observable/index';
|
||||||
import type { ViewBase } from '../../ui/core/view-base';
|
import type { ViewBase } from '../../ui/core/view-base';
|
||||||
|
import { MutationSensitiveArray } from '../mutation-sensitive-array';
|
||||||
|
|
||||||
// This file contains some of Core's hot paths, so attention has been taken to
|
// This file contains some of Core's hot paths, so attention has been taken to
|
||||||
// optimise it. Where specified, optimisations made have been informed based on
|
// optimise it. Where specified, optimisations made have been informed based on
|
||||||
@@ -13,7 +14,7 @@ const timeOrigin = Date.now();
|
|||||||
* optional accesses, so reusing the same one and treating it as immutable
|
* optional accesses, so reusing the same one and treating it as immutable
|
||||||
* avoids unnecessary allocations on a relatively hot path of the library.
|
* avoids unnecessary allocations on a relatively hot path of the library.
|
||||||
*/
|
*/
|
||||||
const emptyArray = [] as const;
|
const emptyArray = new MutationSensitiveArray<ListenerEntry>();
|
||||||
|
|
||||||
export class DOMEvent implements Event {
|
export class DOMEvent implements Event {
|
||||||
/**
|
/**
|
||||||
@@ -223,7 +224,7 @@ export class DOMEvent implements Event {
|
|||||||
* event's cancelable attribute value is false or its preventDefault()
|
* event's cancelable attribute value is false or its preventDefault()
|
||||||
* method was not invoked, and false otherwise.
|
* method was not invoked, and false otherwise.
|
||||||
*/
|
*/
|
||||||
dispatchTo({ target, data, getGlobalEventHandlersPreHandling, getGlobalEventHandlersPostHandling }: { target: Observable; data: EventData; getGlobalEventHandlersPreHandling?: () => readonly ListenerEntry[]; getGlobalEventHandlersPostHandling?: () => readonly ListenerEntry[] }): boolean {
|
dispatchTo({ target, data, getGlobalEventHandlersPreHandling, getGlobalEventHandlersPostHandling }: { target: Observable; data: EventData; getGlobalEventHandlersPreHandling?: () => MutationSensitiveArray<ListenerEntry>; getGlobalEventHandlersPostHandling?: () => MutationSensitiveArray<ListenerEntry> }): boolean {
|
||||||
if (this.eventPhase !== this.NONE) {
|
if (this.eventPhase !== this.NONE) {
|
||||||
throw new Error('Tried to dispatch a dispatching event');
|
throw new Error('Tried to dispatch a dispatching event');
|
||||||
}
|
}
|
||||||
@@ -340,16 +341,31 @@ export class DOMEvent implements Event {
|
|||||||
return this.returnValue;
|
return this.returnValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
private handleEvent({ data, isGlobal, getListenersForType, phase, removeEventListener }: { data: EventData; isGlobal: boolean; getListenersForType: () => readonly ListenerEntry[]; phase: 0 | 1 | 2 | 3; removeEventListener: (eventName: string, callback?: any, thisArg?: any, capture?: boolean) => void }) {
|
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 }) {
|
||||||
// Work on a copy of the array, as any callback could modify the
|
// We want to work on a copy of the array, as any callback could modify
|
||||||
// original array during the loop.
|
// the original array during the loop.
|
||||||
//
|
//
|
||||||
// Cloning the array via spread syntax is up to 180 nanoseconds faster
|
// However, cloning arrays is expensive on this hot path, so we'll do it
|
||||||
// per run than using Array.prototype.slice().
|
// lazily - i.e. only take a clone if a mutation is about to happen.
|
||||||
const listenersForTypeCopy = [...getListenersForType()];
|
// 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();
|
||||||
|
|
||||||
for (let i = listenersForTypeCopy.length - 1; i >= 0; i--) {
|
// Set a listener to clone the array just before any mutations.
|
||||||
const listener = listenersForTypeCopy[i];
|
let listenersLazyCopy: ListenerEntry[] = listenersLive;
|
||||||
|
const doLazyCopy = () => {
|
||||||
|
// Cloning the array via spread syntax is up to 180 nanoseconds
|
||||||
|
// faster per run than using Array.prototype.slice().
|
||||||
|
listenersLazyCopy = [...listenersLive];
|
||||||
|
};
|
||||||
|
listenersLive.once(doLazyCopy);
|
||||||
|
|
||||||
|
// Make sure we remove the listener before we exit the function,
|
||||||
|
// otherwise we may wastefully clone the array.
|
||||||
|
const cleanup = () => listenersLive.removeListener(doLazyCopy);
|
||||||
|
|
||||||
|
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.
|
||||||
@@ -365,7 +381,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 (!getListenersForType().includes(listener)) {
|
if (!listenersLive.includes(listener)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -395,9 +411,12 @@ export class DOMEvent implements Event {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (this.propagationState === EventPropagationState.stopImmediate) {
|
if (this.propagationState === EventPropagationState.stopImmediate) {
|
||||||
|
cleanup();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
cleanup();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
78
packages/core/data/mutation-sensitive-array/index.ts
Normal file
78
packages/core/data/mutation-sensitive-array/index.ts
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
/**
|
||||||
|
* A lightweight extension of Array that calls listeners just before any
|
||||||
|
* mutations. This allows you to lazily take a clone of an array (i.e. use the
|
||||||
|
* array as-is until such time as it mutates).
|
||||||
|
*
|
||||||
|
* This could equally be implemented by adding pre-mutation events into
|
||||||
|
* ObservableArray, but the whole point is to be as lightweight as possible as
|
||||||
|
* its entire purpose is to be used for performance-sensitive tasks.
|
||||||
|
*/
|
||||||
|
export class MutationSensitiveArray<T> extends Array<T> {
|
||||||
|
private readonly listeners: (() => void)[] = [];
|
||||||
|
|
||||||
|
once(listener: () => void): void {
|
||||||
|
const wrapper = () => {
|
||||||
|
listener();
|
||||||
|
this.removeListener(wrapper);
|
||||||
|
};
|
||||||
|
this.addListener(wrapper);
|
||||||
|
}
|
||||||
|
|
||||||
|
addListener(listener: () => void): void {
|
||||||
|
if (!this.listeners.includes(listener)) {
|
||||||
|
this.listeners.push(listener);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
removeListener(listener: () => void): void {
|
||||||
|
const index = this.listeners.indexOf(listener);
|
||||||
|
if (index > -1) {
|
||||||
|
this.listeners.splice(index, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private invalidate(): void {
|
||||||
|
for (const listener of this.listeners) {
|
||||||
|
listener();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Override each mutating Array method so that it invalidates our snapshot.
|
||||||
|
|
||||||
|
pop(): T | undefined {
|
||||||
|
this.invalidate();
|
||||||
|
return super.pop();
|
||||||
|
}
|
||||||
|
push(...items: T[]): number {
|
||||||
|
this.invalidate();
|
||||||
|
return super.push(...items);
|
||||||
|
}
|
||||||
|
reverse(): T[] {
|
||||||
|
this.invalidate();
|
||||||
|
return super.reverse();
|
||||||
|
}
|
||||||
|
shift(): T | undefined {
|
||||||
|
this.invalidate();
|
||||||
|
return super.shift();
|
||||||
|
}
|
||||||
|
sort(compareFn?: (a: T, b: T) => number): this {
|
||||||
|
this.invalidate();
|
||||||
|
return super.sort(compareFn);
|
||||||
|
}
|
||||||
|
splice(start: number, deleteCount: number, ...rest: T[]): T[] {
|
||||||
|
this.invalidate();
|
||||||
|
return super.splice(start, deleteCount, ...rest);
|
||||||
|
}
|
||||||
|
unshift(...items: T[]): number {
|
||||||
|
this.invalidate();
|
||||||
|
return super.unshift(...items);
|
||||||
|
}
|
||||||
|
fill(value: T, start?: number, end?: number): this {
|
||||||
|
this.invalidate();
|
||||||
|
return super.fill(value, start, end);
|
||||||
|
}
|
||||||
|
copyWithin(target: number, start: number, end?: number): this {
|
||||||
|
this.invalidate();
|
||||||
|
return super.copyWithin(target, start, end);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import type { ViewBase } from '../../ui/core/view-base';
|
import type { ViewBase } from '../../ui/core/view-base';
|
||||||
import { DOMEvent } from '../dom-events/dom-event';
|
import { DOMEvent } from '../dom-events/dom-event';
|
||||||
|
import { MutationSensitiveArray } from '../mutation-sensitive-array';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Base event data.
|
* Base event data.
|
||||||
@@ -85,7 +86,7 @@ const _wrappedValues = [new WrappedValue(null), new WrappedValue(null), new Wrap
|
|||||||
|
|
||||||
const _globalEventHandlers: {
|
const _globalEventHandlers: {
|
||||||
[eventClass: string]: {
|
[eventClass: string]: {
|
||||||
[eventName: string]: ListenerEntry[];
|
[eventName: string]: MutationSensitiveArray<ListenerEntry>;
|
||||||
};
|
};
|
||||||
} = {};
|
} = {};
|
||||||
|
|
||||||
@@ -114,7 +115,7 @@ export class Observable implements EventTarget {
|
|||||||
return this._isViewBase;
|
return this._isViewBase;
|
||||||
}
|
}
|
||||||
|
|
||||||
private readonly _observers: { [eventName: string]: ListenerEntry[] } = {};
|
private readonly _observers: { [eventName: string]: MutationSensitiveArray<ListenerEntry> } = {};
|
||||||
|
|
||||||
public get(name: string): any {
|
public get(name: string): any {
|
||||||
return this[name];
|
return this[name];
|
||||||
@@ -313,7 +314,7 @@ export class Observable implements EventTarget {
|
|||||||
_globalEventHandlers[eventClass] = {};
|
_globalEventHandlers[eventClass] = {};
|
||||||
}
|
}
|
||||||
if (!Array.isArray(_globalEventHandlers[eventClass][eventName])) {
|
if (!Array.isArray(_globalEventHandlers[eventClass][eventName])) {
|
||||||
_globalEventHandlers[eventClass][eventName] = [];
|
_globalEventHandlers[eventClass][eventName] = new MutationSensitiveArray();
|
||||||
}
|
}
|
||||||
|
|
||||||
const list = _globalEventHandlers[eventClass][eventName];
|
const list = _globalEventHandlers[eventClass][eventName];
|
||||||
@@ -399,11 +400,11 @@ export class Observable implements EventTarget {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private _getGlobalEventHandlers(data: EventData, eventType: 'First' | ''): ListenerEntry[] {
|
private _getGlobalEventHandlers(data: EventData, eventType: 'First' | ''): MutationSensitiveArray<ListenerEntry> {
|
||||||
const eventClass = data.object?.constructor?.name;
|
const eventClass = data.object?.constructor?.name;
|
||||||
const globalEventHandlersForOwnClass = _globalEventHandlers[eventClass]?.[`${data.eventName}${eventType}`] ?? [];
|
const globalEventHandlersForOwnClass = _globalEventHandlers[eventClass]?.[`${data.eventName}${eventType}`] ?? [];
|
||||||
const globalEventHandlersForAllClasses = _globalEventHandlers['*']?.[`${data.eventName}${eventType}`] ?? [];
|
const globalEventHandlersForAllClasses = _globalEventHandlers['*']?.[`${data.eventName}${eventType}`] ?? [];
|
||||||
return [...globalEventHandlersForOwnClass, ...globalEventHandlersForAllClasses];
|
return new MutationSensitiveArray(...globalEventHandlersForOwnClass, ...globalEventHandlersForAllClasses);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -440,14 +441,14 @@ export class Observable implements EventTarget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public getEventList(eventName: string, createIfNeeded?: boolean): ListenerEntry[] | undefined {
|
public getEventList(eventName: string, createIfNeeded?: boolean): MutationSensitiveArray<ListenerEntry> | undefined {
|
||||||
if (!eventName) {
|
if (!eventName) {
|
||||||
throw new TypeError('EventName must be valid string.');
|
throw new TypeError('EventName must be valid string.');
|
||||||
}
|
}
|
||||||
|
|
||||||
let list = this._observers[eventName];
|
let list = this._observers[eventName];
|
||||||
if (!list && createIfNeeded) {
|
if (!list && createIfNeeded) {
|
||||||
list = [];
|
list = new MutationSensitiveArray();
|
||||||
this._observers[eventName] = list;
|
this._observers[eventName] = list;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user