@@ -27,11 +27,10 @@ interface EventTaskData extends TaskData {
2727 // use global callback or not
2828 readonly useG ?: boolean ;
2929 taskData ?: any ;
30- removeAbortListener ?: VoidFunction | null ;
3130}
3231
3332/** @internal **/
34- interface InternalTaskData {
33+ interface InternalGlobalTaskData {
3534 // This is used internally to avoid duplicating event listeners on
3635 // the same target when the event name is the same, such as when
3736 // `addEventListener` is called multiple times on the `document`
@@ -46,6 +45,36 @@ interface InternalTaskData {
4645 options ?: any ; // boolean | AddEventListenerOptions
4746}
4847
48+ /**
49+ * The `scheduleEventTask` function returns an `EventTask` object.
50+ * However, we also store some task-related information on the task
51+ * itself, such as the task target, for easy access when the task is
52+ * manually canceled or for other purposes. This internal storage is
53+ * used solely for enhancing our understanding of which properties are
54+ * being assigned to the task.
55+ *
56+ * @internal
57+ */
58+ interface InternalEventTask extends EventTask {
59+ removeAbortListener ?: VoidFunction | null ;
60+ // `target` is the actual event target on which `addEventListener`
61+ // is being called for this specific task.
62+ target ?: any ;
63+ eventName ?: string ;
64+ capture ?: boolean ;
65+ // Not changing the type to avoid any regressions.
66+ options ?: any ; // boolean | AddEventListenerOptions
67+ // `isRemoved` is associated with a specific task and indicates whether
68+ // that task was canceled and removed from the event target to prevent
69+ // its invocation if dispatched later.
70+ isRemoved ?: boolean ;
71+ allRemoved ?: boolean ;
72+ }
73+
74+ // Note that passive event listeners are now supported by most modern browsers,
75+ // including Chrome, Firefox, Safari, and Edge. There's a pending change that
76+ // would remove support for legacy browsers by zone.js. Removing `passiveSupported`
77+ // from the codebase will reduce the final code size for existing apps that still use zone.js.
4978let passiveSupported = false ;
5079
5180if ( typeof window !== 'undefined' ) {
@@ -263,9 +292,15 @@ export function patchEventTarget(
263292
264293 const eventNameToString = patchOptions && patchOptions . eventNameToString ;
265294
266- // a shared global taskData to pass data for scheduleEventTask
267- // so we do not need to create a new object just for pass some data
268- const taskData : InternalTaskData = { } ;
295+ // We use a shared global `taskData` to pass data for `scheduleEventTask`,
296+ // eliminating the need to create a new object solely for passing data.
297+ // WARNING: This object has a static lifetime, meaning it is not created
298+ // each time `addEventListener` is called. It is instantiated only once
299+ // and captured by reference inside the `addEventListener` and
300+ // `removeEventListener` functions. Do not add any new properties to this
301+ // object, as doing so would necessitate maintaining the information
302+ // between `addEventListener` calls.
303+ const taskData : InternalGlobalTaskData = { } ;
269304
270305 const nativeAddEventListener = ( proto [ zoneSymbolAddEventListener ] = proto [ ADD_EVENT_LISTENER ] ) ;
271306 const nativeRemoveEventListener = ( proto [ zoneSymbol ( REMOVE_EVENT_LISTENER ) ] =
@@ -322,12 +357,18 @@ export function patchEventTarget(
322357 ) ;
323358 } ;
324359
325- const customCancelGlobal = function ( task : any ) {
360+ /**
361+ * In the context of events and listeners, this function will be
362+ * called at the end by `cancelTask`, which, in turn, calls `task.cancelFn`.
363+ * Cancelling a task is primarily used to remove event listeners from
364+ * the task target.
365+ */
366+ const customCancelGlobal = function ( task : InternalEventTask ) {
326367 // if task is not marked as isRemoved, this call is directly
327368 // from Zone.prototype.cancelTask, we should remove the task
328369 // from tasksList of target first
329370 if ( ! task . isRemoved ) {
330- const symbolEventNames = zoneSymbolEventNames [ task . eventName ] ;
371+ const symbolEventNames = zoneSymbolEventNames [ task . eventName ! ] ;
331372 let symbolEventName ;
332373 if ( symbolEventNames ) {
333374 symbolEventName = symbolEventNames [ task . capture ? TRUE_STR : FALSE_STR ] ;
@@ -340,6 +381,10 @@ export function patchEventTarget(
340381 existingTasks . splice ( i , 1 ) ;
341382 // set isRemoved to data for faster invokeTask check
342383 task . isRemoved = true ;
384+ if ( task . removeAbortListener ) {
385+ task . removeAbortListener ( ) ;
386+ task . removeAbortListener = null ;
387+ }
343388 if ( existingTasks . length === 0 ) {
344389 // all tasks for the eventName + capture have gone,
345390 // remove globalZoneAwareCallback and remove the task cache from target
@@ -563,7 +608,7 @@ export function patchEventTarget(
563608 // which in turn calls the native `addEventListener`. This is why `taskData.options`
564609 // is updated before scheduling the task, as `customScheduleGlobal` uses
565610 // `taskData.options` to pass it to the native `addEventListener`.
566- const task : any = zone . scheduleEventTask (
611+ const task : InternalEventTask = zone . scheduleEventTask (
567612 source ,
568613 delegate ,
569614 data ,
@@ -583,9 +628,7 @@ export function patchEventTarget(
583628 // as it creates a closure that captures `task`. This closure retains a reference to the
584629 // `task` object even after it goes out of scope, preventing `task` from being garbage
585630 // collected.
586- if ( data ) {
587- data . removeAbortListener = ( ) => signal . removeEventListener ( 'abort' , onAbort ) ;
588- }
631+ task . removeAbortListener = ( ) => signal . removeEventListener ( 'abort' , onAbort ) ;
589632 }
590633
591634 // should clear taskData.target to avoid memory leak
@@ -670,18 +713,22 @@ export function patchEventTarget(
670713 if ( symbolEventNames ) {
671714 symbolEventName = symbolEventNames [ capture ? TRUE_STR : FALSE_STR ] ;
672715 }
673- const existingTasks : Task [ ] = symbolEventName && target [ symbolEventName ] ;
716+ const existingTasks : InternalEventTask [ ] = symbolEventName && target [ symbolEventName ] ;
717+ // `existingTasks` may not exist if the `addEventListener` was called before
718+ // it was patched by zone.js. Please refer to the attached issue for
719+ // clarification, particularly after the `if` condition, before calling
720+ // the native `removeEventListener`.
674721 if ( existingTasks ) {
675722 for ( let i = 0 ; i < existingTasks . length ; i ++ ) {
676723 const existingTask = existingTasks [ i ] ;
677724 if ( compare ( existingTask , delegate ) ) {
678725 existingTasks . splice ( i , 1 ) ;
679726 // set isRemoved to data for faster invokeTask check
680- ( existingTask as any ) . isRemoved = true ;
727+ existingTask . isRemoved = true ;
681728 if ( existingTasks . length === 0 ) {
682729 // all tasks for the eventName + capture have gone,
683730 // remove globalZoneAwareCallback and remove the task cache from target
684- ( existingTask as any ) . allRemoved = true ;
731+ existingTask . allRemoved = true ;
685732 target [ symbolEventName ] = null ;
686733 // in the target, we have an event listener which is added by on_property
687734 // such as target.onclick = function() {}, so we need to clear this internal
@@ -693,15 +740,11 @@ export function patchEventTarget(
693740 target [ onPropertySymbol ] = null ;
694741 }
695742 }
696-
697- // Note that `removeAllListeners` would ultimately call `removeEventListener`,
698- // so we're safe to remove the abort listener only once here.
699- const taskData = existingTask . data as EventTaskData ;
700- if ( taskData ?. removeAbortListener ) {
701- taskData . removeAbortListener ( ) ;
702- taskData . removeAbortListener = null ;
703- }
704-
743+ // In all other conditions, when `addEventListener` is called after being
744+ // patched by zone.js, we would always find an event task on the `EventTarget`.
745+ // This will trigger `cancelFn` on the `existingTask`, leading to `customCancelGlobal`,
746+ // which ultimately removes an event listener and cleans up the abort listener
747+ // (if an `AbortSignal` was provided when scheduling a task).
705748 existingTask . zone . cancelTask ( existingTask ) ;
706749 if ( returnTarget ) {
707750 return target ;
@@ -710,10 +753,12 @@ export function patchEventTarget(
710753 }
711754 }
712755 }
713- // issue 930, didn't find the event name or callback
714- // from zone kept existingTasks, the callback maybe
715- // added outside of zone, we need to call native removeEventListener
716- // to try to remove it.
756+ // https://github.com/angular/zone.js/issues/930
757+ // We may encounter a situation where the `addEventListener` was
758+ // called on the event target before zone.js is loaded, resulting
759+ // in no task being stored on the event target due to its invocation
760+ // of the native implementation. In this scenario, we simply need to
761+ // invoke the native `removeEventListener`.
717762 return nativeRemoveEventListener . apply ( this , arguments ) ;
718763 } ;
719764
0 commit comments