mirror of
https://github.com/NativeScript/NativeScript.git
synced 2025-11-05 13:26:48 +08:00
fix: ditch MutationSensitiveArray
This commit is contained in:
@@ -1,5 +1,4 @@
|
|||||||
import type { EventData, ListenerEntry, Observable } from '../observable/index';
|
import type { EventData, ListenerEntry, Observable } from '../observable/index';
|
||||||
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 +12,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 = new MutationSensitiveArray<ListenerEntry>();
|
const emptyArray: ListenerEntry[] = [];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Recycling the event path array rather than allocating a new one each time
|
* Recycling the event path array rather than allocating a new one each time
|
||||||
@@ -62,7 +61,7 @@ export class DOMEvent implements Event {
|
|||||||
Object.defineProperty(DOMEvent.prototype, 'currentTarget', { value: null, writable: true });
|
Object.defineProperty(DOMEvent.prototype, 'currentTarget', { value: null, writable: true });
|
||||||
Object.defineProperty(DOMEvent.prototype, 'target', { value: null, writable: true });
|
Object.defineProperty(DOMEvent.prototype, 'target', { value: null, writable: true });
|
||||||
Object.defineProperty(DOMEvent.prototype, 'propagationState', { value: EventPropagationState.resume, writable: true });
|
Object.defineProperty(DOMEvent.prototype, 'propagationState', { value: EventPropagationState.resume, writable: true });
|
||||||
Object.defineProperty(DOMEvent.prototype, 'listenersLive', { value: emptyArray, writable: true });
|
Object.defineProperty(DOMEvent.prototype, 'listeners', { value: emptyArray, writable: true });
|
||||||
Object.defineProperty(DOMEvent.prototype, 'listenersLazyCopy', { value: emptyArray, writable: true });
|
Object.defineProperty(DOMEvent.prototype, 'listenersLazyCopy', { value: emptyArray, writable: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -152,8 +151,7 @@ export class DOMEvent implements Event {
|
|||||||
// lazily - i.e. only take a clone if a mutation is about to happen.
|
// 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
|
// This optimisation is particularly worth doing as it's very rare that
|
||||||
// an event listener callback will end up modifying the listeners array.
|
// an event listener callback will end up modifying the listeners array.
|
||||||
private declare listenersLive: MutationSensitiveArray<ListenerEntry>;
|
private declare listeners: ListenerEntry[];
|
||||||
private declare listenersLazyCopy: ListenerEntry[];
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the event's timestamp as the number of milliseconds measured
|
* Returns the event's timestamp as the number of milliseconds measured
|
||||||
@@ -267,7 +265,7 @@ export class DOMEvent implements Event {
|
|||||||
*/
|
*/
|
||||||
// Taking multiple params rather than a single property bag saves about 100
|
// Taking multiple params rather than a single property bag saves about 100
|
||||||
// nanoseconds per call.
|
// nanoseconds per call.
|
||||||
dispatchTo(target: Observable, data: EventData, getGlobalEventHandlersPreHandling?: () => MutationSensitiveArray<ListenerEntry>, getGlobalEventHandlersPostHandling?: () => MutationSensitiveArray<ListenerEntry>): boolean {
|
dispatchTo(target: Observable, data: EventData, getGlobalEventHandlersPreHandling?: () => ListenerEntry[], getGlobalEventHandlersPostHandling?: () => ListenerEntry[]): boolean {
|
||||||
if (this.eventPhase !== DOMEvent.NONE) {
|
if (this.eventPhase !== DOMEvent.NONE) {
|
||||||
throw new Error('Tried to dispatch a dispatching event');
|
throw new Error('Tried to dispatch a dispatching event');
|
||||||
}
|
}
|
||||||
@@ -308,7 +306,7 @@ 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.listeners = getGlobalEventHandlersPreHandling?.() || emptyArray;
|
||||||
this.handleEvent(data, true, DOMEvent.CAPTURING_PHASE, removeGlobalEventListener, target.constructor);
|
this.handleEvent(data, true, DOMEvent.CAPTURING_PHASE, removeGlobalEventListener, target.constructor);
|
||||||
|
|
||||||
const eventPath = this.getEventPath(target, 'capture');
|
const eventPath = this.getEventPath(target, 'capture');
|
||||||
@@ -321,7 +319,7 @@ export class DOMEvent implements Event {
|
|||||||
this.currentTarget = currentTarget;
|
this.currentTarget = currentTarget;
|
||||||
this.eventPhase = this.target === this.currentTarget ? DOMEvent.AT_TARGET : DOMEvent.CAPTURING_PHASE;
|
this.eventPhase = this.target === this.currentTarget ? DOMEvent.AT_TARGET : DOMEvent.CAPTURING_PHASE;
|
||||||
|
|
||||||
this.listenersLazyCopy = this.listenersLive = currentTarget.getEventList(this.type) || emptyArray;
|
this.listeners = currentTarget.getEventList(this.type) || emptyArray;
|
||||||
this.handleEvent(data, false, DOMEvent.CAPTURING_PHASE, currentTarget.removeEventListener, currentTarget);
|
this.handleEvent(data, false, DOMEvent.CAPTURING_PHASE, currentTarget.removeEventListener, currentTarget);
|
||||||
|
|
||||||
if (this.propagationState !== EventPropagationState.resume) {
|
if (this.propagationState !== EventPropagationState.resume) {
|
||||||
@@ -336,7 +334,7 @@ export class DOMEvent implements Event {
|
|||||||
const currentTarget = eventPath[i];
|
const currentTarget = eventPath[i];
|
||||||
this.eventPhase = this.target === this.currentTarget ? DOMEvent.AT_TARGET : DOMEvent.BUBBLING_PHASE;
|
this.eventPhase = this.target === this.currentTarget ? DOMEvent.AT_TARGET : DOMEvent.BUBBLING_PHASE;
|
||||||
|
|
||||||
this.listenersLazyCopy = this.listenersLive = currentTarget.getEventList(this.type) || emptyArray;
|
this.listeners = currentTarget.getEventList(this.type) || emptyArray;
|
||||||
this.handleEvent(data, false, DOMEvent.BUBBLING_PHASE, currentTarget.removeEventListener, currentTarget);
|
this.handleEvent(data, false, DOMEvent.BUBBLING_PHASE, currentTarget.removeEventListener, currentTarget);
|
||||||
|
|
||||||
if (this.propagationState !== EventPropagationState.resume) {
|
if (this.propagationState !== EventPropagationState.resume) {
|
||||||
@@ -357,41 +355,21 @@ export class DOMEvent implements Event {
|
|||||||
this.eventPhase = DOMEvent.BUBBLING_PHASE;
|
this.eventPhase = DOMEvent.BUBBLING_PHASE;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.listenersLazyCopy = this.listenersLive = getGlobalEventHandlersPostHandling?.() || emptyArray;
|
this.listeners = getGlobalEventHandlersPostHandling?.() || emptyArray;
|
||||||
this.handleEvent(data, true, DOMEvent.BUBBLING_PHASE, removeGlobalEventListener, target.constructor);
|
this.handleEvent(data, true, DOMEvent.BUBBLING_PHASE, removeGlobalEventListener, target.constructor);
|
||||||
|
|
||||||
this.resetForRedispatch();
|
this.resetForRedispatch();
|
||||||
return !this.defaultPrevented;
|
return !this.defaultPrevented;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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 dispatchTo().
|
|
||||||
//
|
|
||||||
// Creating it on the prototype and calling with the context instead saves a
|
|
||||||
// further 125 nanoseconds per run of dispatchTo().
|
|
||||||
//
|
|
||||||
// Creating it on the prototype and binding the context instead saves a
|
|
||||||
// further 30 nanoseconds per run of dispatchTo().
|
|
||||||
private beforeCurrentListenersMutation() {
|
|
||||||
// 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.beforeMutation = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Taking multiple params instead of a single property bag saves 250
|
// Taking multiple params instead of a single property bag saves 250
|
||||||
// nanoseconds per dispatchTo() call.
|
// nanoseconds per dispatchTo() call.
|
||||||
private handleEvent(data: EventData, isGlobal: boolean, phase: 0 | 1 | 2 | 3, removeEventListener: (eventName: string, callback?: any, thisArg?: any, capture?: boolean) => void, removeEventListenerContext: unknown) {
|
private handleEvent(data: EventData, isGlobal: boolean, phase: 0 | 1 | 2 | 3, removeEventListener: (eventName: string, callback?: any, thisArg?: any, capture?: boolean) => void, removeEventListenerContext: unknown) {
|
||||||
// Set a listener to clone the array just before any mutations.
|
// Clone the array just before any mutations.
|
||||||
//
|
const listeners = [...this.listeners];
|
||||||
// Lazy-binding this (binding it at the time of calling, rather than
|
|
||||||
// eagerly) unexpectedly seems to slow things down - v8 may be applying
|
|
||||||
// some sort of optimisation or something.
|
|
||||||
this.listenersLive.beforeMutation = this.beforeCurrentListenersMutation.bind(this);
|
|
||||||
|
|
||||||
for (let i = this.listenersLazyCopy.length - 1; i >= 0; i--) {
|
for (let i = listeners.length - 1; i >= 0; i--) {
|
||||||
const listener = this.listenersLazyCopy[i];
|
const listener = listeners[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.
|
||||||
@@ -417,7 +395,7 @@ export class DOMEvent implements Event {
|
|||||||
// MutationSensitiveArray called afterRemoval, similar to
|
// MutationSensitiveArray called afterRemoval, similar to
|
||||||
// beforeMutation) to allow O(1) lookup, but it went 1000 ns slower
|
// beforeMutation) to allow O(1) lookup, but it went 1000 ns slower
|
||||||
// in practice, so it stays!
|
// in practice, so it stays!
|
||||||
if (!this.listenersLive.includes(listener)) {
|
if (!this.listeners.includes(listener)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -458,7 +436,6 @@ export class DOMEvent implements Event {
|
|||||||
|
|
||||||
// Make sure we clear the callback before we exit the function,
|
// Make sure we clear the callback before we exit the function,
|
||||||
// otherwise we may wastefully clone the array on future mutations.
|
// otherwise we may wastefully clone the array on future mutations.
|
||||||
this.listenersLive.beforeMutation = null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -472,7 +449,6 @@ export class DOMEvent implements Event {
|
|||||||
this.target = null;
|
this.target = null;
|
||||||
this.eventPhase = DOMEvent.NONE;
|
this.eventPhase = DOMEvent.NONE;
|
||||||
this.propagationState = EventPropagationState.resume;
|
this.propagationState = EventPropagationState.resume;
|
||||||
this.listenersLive = emptyArray;
|
this.listeners = emptyArray;
|
||||||
this.listenersLazyCopy = emptyArray;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,49 +0,0 @@
|
|||||||
/**
|
|
||||||
* 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). In other worse, "copy on write".
|
|
||||||
*
|
|
||||||
* 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> {
|
|
||||||
beforeMutation: (() => void) | null = null;
|
|
||||||
|
|
||||||
pop(): T | undefined {
|
|
||||||
this.beforeMutation?.();
|
|
||||||
return super.pop();
|
|
||||||
}
|
|
||||||
push(...items: T[]): number {
|
|
||||||
this.beforeMutation?.();
|
|
||||||
return super.push(...items);
|
|
||||||
}
|
|
||||||
reverse(): T[] {
|
|
||||||
this.beforeMutation?.();
|
|
||||||
return super.reverse();
|
|
||||||
}
|
|
||||||
shift(): T | undefined {
|
|
||||||
this.beforeMutation?.();
|
|
||||||
return super.shift();
|
|
||||||
}
|
|
||||||
sort(compareFn?: (a: T, b: T) => number): this {
|
|
||||||
this.beforeMutation?.();
|
|
||||||
return super.sort(compareFn);
|
|
||||||
}
|
|
||||||
splice(start: number, deleteCount: number, ...rest: T[]): T[] {
|
|
||||||
this.beforeMutation?.();
|
|
||||||
return super.splice(start, deleteCount, ...rest);
|
|
||||||
}
|
|
||||||
unshift(...items: T[]): number {
|
|
||||||
this.beforeMutation?.();
|
|
||||||
return super.unshift(...items);
|
|
||||||
}
|
|
||||||
fill(value: T, start?: number, end?: number): this {
|
|
||||||
this.beforeMutation?.();
|
|
||||||
return super.fill(value, start, end);
|
|
||||||
}
|
|
||||||
copyWithin(target: number, start: number, end?: number): this {
|
|
||||||
this.beforeMutation?.();
|
|
||||||
return super.copyWithin(target, start, end);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
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.
|
||||||
@@ -86,7 +85,7 @@ const _wrappedValues = [new WrappedValue(null), new WrappedValue(null), new Wrap
|
|||||||
|
|
||||||
const _globalEventHandlers: {
|
const _globalEventHandlers: {
|
||||||
[eventClass: string]: {
|
[eventClass: string]: {
|
||||||
[eventName: string]: MutationSensitiveArray<ListenerEntry>;
|
[eventName: string]: ListenerEntry[];
|
||||||
};
|
};
|
||||||
} = {};
|
} = {};
|
||||||
|
|
||||||
@@ -115,7 +114,7 @@ export class Observable implements EventTarget {
|
|||||||
return this._isViewBase;
|
return this._isViewBase;
|
||||||
}
|
}
|
||||||
|
|
||||||
private readonly _observers: { [eventName: string]: MutationSensitiveArray<ListenerEntry> } = {};
|
private readonly _observers: { [eventName: string]: ListenerEntry[] } = {};
|
||||||
|
|
||||||
public get(name: string): any {
|
public get(name: string): any {
|
||||||
return this[name];
|
return this[name];
|
||||||
@@ -314,7 +313,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] = new MutationSensitiveArray();
|
_globalEventHandlers[eventClass][eventName] = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
const list = _globalEventHandlers[eventClass][eventName];
|
const list = _globalEventHandlers[eventClass][eventName];
|
||||||
@@ -400,11 +399,11 @@ export class Observable implements EventTarget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private _getGlobalEventHandlers(data: EventData, eventType: 'First' | ''): MutationSensitiveArray<ListenerEntry> {
|
private _getGlobalEventHandlers(data: EventData, eventType: 'First' | ''): 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 new MutationSensitiveArray(...globalEventHandlersForOwnClass, ...globalEventHandlersForAllClasses);
|
return [...globalEventHandlersForOwnClass, ...globalEventHandlersForAllClasses];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -441,14 +440,14 @@ export class Observable implements EventTarget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public getEventList(eventName: string, createIfNeeded?: boolean): MutationSensitiveArray<ListenerEntry> | undefined {
|
public getEventList(eventName: string, createIfNeeded?: boolean): 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 = new MutationSensitiveArray();
|
list = [];
|
||||||
this._observers[eventName] = list;
|
this._observers[eventName] = list;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user