1
1
import type { EventData , ListenerEntry , Observable } from '../observable/index' ;
2
2
import type { ViewBase } from '../../ui/core/view-base' ;
3
+ import { MutationSensitiveArray } from '../mutation-sensitive-array' ;
3
4
4
5
// This file contains some of Core's hot paths, so attention has been taken to
5
6
// optimise it. Where specified, optimisations made have been informed based on
@@ -13,7 +14,7 @@ const timeOrigin = Date.now();
13
14
* optional accesses, so reusing the same one and treating it as immutable
14
15
* avoids unnecessary allocations on a relatively hot path of the library.
15
16
*/
16
- const emptyArray = [ ] as const ;
17
+ const emptyArray = new MutationSensitiveArray < ListenerEntry > ( ) ;
17
18
18
19
export class DOMEvent implements Event {
19
20
/**
@@ -223,7 +224,7 @@ export class DOMEvent implements Event {
223
224
* event's cancelable attribute value is false or its preventDefault()
224
225
* method was not invoked, and false otherwise.
225
226
*/
226
- dispatchTo ( { target, data, getGlobalEventHandlersPreHandling, getGlobalEventHandlersPostHandling } : { target : Observable ; data : EventData ; getGlobalEventHandlersPreHandling ?: ( ) => readonly ListenerEntry [ ] ; getGlobalEventHandlersPostHandling ?: ( ) => readonly ListenerEntry [ ] } ) : boolean {
227
+ dispatchTo ( { target, data, getGlobalEventHandlersPreHandling, getGlobalEventHandlersPostHandling } : { target : Observable ; data : EventData ; getGlobalEventHandlersPreHandling ?: ( ) => MutationSensitiveArray < ListenerEntry > ; getGlobalEventHandlersPostHandling ?: ( ) => MutationSensitiveArray < ListenerEntry > } ) : boolean {
227
228
if ( this . eventPhase !== this . NONE ) {
228
229
throw new Error ( 'Tried to dispatch a dispatching event' ) ;
229
230
}
@@ -340,16 +341,31 @@ export class DOMEvent implements Event {
340
341
return this . returnValue ;
341
342
}
342
343
343
- 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 } ) {
344
- // Work on a copy of the array, as any callback could modify the
345
- // original array during the loop.
344
+ 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 } ) {
345
+ // We want to work on a copy of the array, as any callback could modify
346
+ // the original array during the loop.
346
347
//
347
- // Cloning the array via spread syntax is up to 180 nanoseconds faster
348
- // per run than using Array.prototype.slice().
349
- const listenersForTypeCopy = [ ...getListenersForType ( ) ] ;
348
+ // However, cloning arrays is expensive on this hot path, so we'll do it
349
+ // lazily - i.e. only take a clone if a mutation is about to happen.
350
+ // This optimisation is particularly worth doing as it's very rare that
351
+ // an event listener callback will end up modifying the listeners array.
352
+ const listenersLive : MutationSensitiveArray < ListenerEntry > = getListenersForType ( ) ;
353
+
354
+ // Set a listener to clone the array just before any mutations.
355
+ let listenersLazyCopy : ListenerEntry [ ] = listenersLive ;
356
+ const doLazyCopy = ( ) => {
357
+ // Cloning the array via spread syntax is up to 180 nanoseconds
358
+ // faster per run than using Array.prototype.slice().
359
+ listenersLazyCopy = [ ...listenersLive ] ;
360
+ } ;
361
+ listenersLive . once ( doLazyCopy ) ;
362
+
363
+ // Make sure we remove the listener before we exit the function,
364
+ // otherwise we may wastefully clone the array.
365
+ const cleanup = ( ) => listenersLive . removeListener ( doLazyCopy ) ;
350
366
351
- for ( let i = listenersForTypeCopy . length - 1 ; i >= 0 ; i -- ) {
352
- const listener = listenersForTypeCopy [ i ] ;
367
+ for ( let i = listenersLazyCopy . length - 1 ; i >= 0 ; i -- ) {
368
+ const listener = listenersLazyCopy [ i ] ;
353
369
354
370
// Assigning variables this old-fashioned way is up to 50
355
371
// nanoseconds faster per run than ESM destructuring syntax.
@@ -365,7 +381,7 @@ export class DOMEvent implements Event {
365
381
// We simply use a strict equality check here because we trust that
366
382
// the listeners provider will never allow two deeply-equal
367
383
// listeners into the array.
368
- if ( ! getListenersForType ( ) . includes ( listener ) ) {
384
+ if ( ! listenersLive . includes ( listener ) ) {
369
385
continue ;
370
386
}
371
387
@@ -395,9 +411,12 @@ export class DOMEvent implements Event {
395
411
}
396
412
397
413
if ( this . propagationState === EventPropagationState . stopImmediate ) {
414
+ cleanup ( ) ;
398
415
return ;
399
416
}
400
417
}
418
+
419
+ cleanup ( ) ;
401
420
}
402
421
}
403
422
0 commit comments