mirror of
https://github.com/angular/angular.git
synced 2026-09-14 13:54:52 +08:00
refactor(core): separate out DOM listener logic (#60549)
Moves the logic for listening to a DOM event out into a separate function for easier reuse. PR Close #60549
This commit is contained in:
committed by
Alex Rickabaugh
parent
defefae4b5
commit
acf84db778
@@ -22,7 +22,6 @@ import {APP_BOOTSTRAP_LISTENER, ApplicationRef} from '../application/application
|
||||
import {ENVIRONMENT_INITIALIZER, Injector} from '../di';
|
||||
import {inject} from '../di/injector_compatibility';
|
||||
import {Provider} from '../di/interface/provider';
|
||||
import {setStashFn} from '../render3/instructions/listener';
|
||||
import {RElement, RNode} from '../render3/interfaces/renderer_dom';
|
||||
import {CLEANUP, LView, TView} from '../render3/interfaces/view';
|
||||
import {unwrapRNode} from '../render3/util/view_utils';
|
||||
@@ -45,6 +44,7 @@ import {APP_ID} from '../application/application_tokens';
|
||||
import {performanceMarkFeature} from '../util/performance';
|
||||
import {triggerHydrationFromBlockName} from '../defer/triggering';
|
||||
import {isIncrementalHydrationEnabled} from './utils';
|
||||
import {setStashFn} from '../render3/view/listeners';
|
||||
|
||||
/** Apps in which we've enabled event replay.
|
||||
* This is to prevent initializing event replay more than once per app.
|
||||
|
||||
@@ -8,34 +8,15 @@
|
||||
|
||||
import {TNode, TNodeType} from '../interfaces/node';
|
||||
import {GlobalTargetResolver, Renderer} from '../interfaces/renderer';
|
||||
import {RElement, RNode} from '../interfaces/renderer_dom';
|
||||
import {isDirectiveHost} from '../interfaces/type_checks';
|
||||
import {CLEANUP, CONTEXT, LView, RENDERER, TView} from '../interfaces/view';
|
||||
import {LView, RENDERER, TView} from '../interfaces/view';
|
||||
import {assertTNodeType} from '../node_assert';
|
||||
import {getCurrentDirectiveDef, getCurrentTNode, getLView, getTView} from '../state';
|
||||
import {
|
||||
getNativeByTNode,
|
||||
getOrCreateLViewCleanup,
|
||||
getOrCreateTViewCleanup,
|
||||
unwrapRNode,
|
||||
} from '../util/view_utils';
|
||||
import {getOrCreateLViewCleanup, getOrCreateTViewCleanup} from '../util/view_utils';
|
||||
|
||||
import {listenToOutput} from '../view/directive_outputs';
|
||||
import {wrapListener} from '../view/listeners';
|
||||
import {listenToDomEvent, wrapListener} from '../view/listeners';
|
||||
import {loadComponentRenderer} from './shared';
|
||||
|
||||
/**
|
||||
* Contains a reference to a function that disables event replay feature
|
||||
* for server-side rendered applications. This function is overridden with
|
||||
* an actual implementation when the event replay feature is enabled via
|
||||
* `withEventReplay()` call.
|
||||
*/
|
||||
let stashEventListener = (el: RNode, eventName: string, listenerFn: (e?: any) => any) => {};
|
||||
|
||||
export function setStashFn(fn: typeof stashEventListener) {
|
||||
stashEventListener = fn;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds an event listener to the current node.
|
||||
*
|
||||
@@ -103,42 +84,6 @@ export function ɵɵsyntheticHostListener(
|
||||
return ɵɵsyntheticHostListener;
|
||||
}
|
||||
|
||||
/**
|
||||
* A utility function that checks if a given element has already an event handler registered for an
|
||||
* event with a specified name. The TView.cleanup data structure is used to find out which events
|
||||
* are registered for a given element.
|
||||
*/
|
||||
function findExistingListener(
|
||||
tView: TView,
|
||||
lView: LView,
|
||||
eventName: string,
|
||||
tNodeIdx: number,
|
||||
): ((e?: any) => any) | null {
|
||||
const tCleanup = tView.cleanup;
|
||||
if (tCleanup != null) {
|
||||
for (let i = 0; i < tCleanup.length - 1; i += 2) {
|
||||
const cleanupEventName = tCleanup[i];
|
||||
if (cleanupEventName === eventName && tCleanup[i + 1] === tNodeIdx) {
|
||||
// We have found a matching event name on the same node but it might not have been
|
||||
// registered yet, so we must explicitly verify entries in the LView cleanup data
|
||||
// structures.
|
||||
const lCleanup = lView[CLEANUP]!;
|
||||
const listenerIdxInLCleanup = tCleanup[i + 2];
|
||||
return lCleanup.length > listenerIdxInLCleanup ? lCleanup[listenerIdxInLCleanup] : null;
|
||||
}
|
||||
// TView.cleanup can have a mix of 4-elements entries (for event handler cleanups) or
|
||||
// 2-element entries (for directive and queries destroy hooks). As such we can encounter
|
||||
// blocks of 4 or 2 items in the tView.cleanup and this is why we iterate over 2 elements
|
||||
// first and jump another 2 elements if we detect listeners cleanup (4 elements). Also check
|
||||
// documentation of TView.cleanup for more details of this data structure layout.
|
||||
if (typeof cleanupEventName === 'string') {
|
||||
i += 2;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export function listenerInternal(
|
||||
tView: TView,
|
||||
lView: LView<{} | null>,
|
||||
@@ -148,10 +93,7 @@ export function listenerInternal(
|
||||
listenerFn: (e?: any) => any,
|
||||
eventTargetResolver?: GlobalTargetResolver,
|
||||
): void {
|
||||
const isTNodeDirectiveHost = isDirectiveHost(tNode);
|
||||
const firstCreatePass = tView.firstCreatePass;
|
||||
const tCleanup = firstCreatePass ? getOrCreateTViewCleanup(tView) : null;
|
||||
const context = lView[CONTEXT];
|
||||
const tCleanup = tView.firstCreatePass ? getOrCreateTViewCleanup(tView) : null;
|
||||
|
||||
// When the ɵɵlistener instruction was generated and is executed we know that there is either a
|
||||
// native listener or a directive output on this element. As such we we know that we will have to
|
||||
@@ -161,63 +103,30 @@ export function listenerInternal(
|
||||
ngDevMode && assertTNodeType(tNode, TNodeType.AnyRNode | TNodeType.AnyContainer);
|
||||
|
||||
let processOutputs = true;
|
||||
let wrappedListener: ((e?: any) => any) | null = null;
|
||||
|
||||
// Adding a native event listener is applicable when:
|
||||
// - The corresponding TNode represents a DOM element.
|
||||
// - The event target has a resolver (usually resulting in a global object,
|
||||
// such as `window` or `document`).
|
||||
if (tNode.type & TNodeType.AnyRNode || eventTargetResolver) {
|
||||
const native = getNativeByTNode(tNode, lView) as RElement;
|
||||
const target = eventTargetResolver ? eventTargetResolver(native) : native;
|
||||
const lCleanupIndex = lCleanup.length;
|
||||
const idxOrTargetGetter = eventTargetResolver
|
||||
? (_lView: LView) => eventTargetResolver(unwrapRNode(_lView[tNode.index]))
|
||||
: tNode.index;
|
||||
wrappedListener ??= wrapListener(tNode, lView, listenerFn);
|
||||
const hasCoalescedDomEvent = listenToDomEvent(
|
||||
tNode,
|
||||
lView,
|
||||
eventTargetResolver,
|
||||
lCleanup,
|
||||
tView,
|
||||
eventName,
|
||||
wrappedListener,
|
||||
listenerFn,
|
||||
renderer,
|
||||
tCleanup,
|
||||
);
|
||||
|
||||
// In order to match current behavior, native DOM event listeners must be added for all
|
||||
// events (including outputs).
|
||||
|
||||
// There might be cases where multiple directives on the same element try to register an event
|
||||
// handler function for the same event. In this situation we want to avoid registration of
|
||||
// several native listeners as each registration would be intercepted by NgZone and
|
||||
// trigger change detection. This would mean that a single user action would result in several
|
||||
// change detections being invoked. To avoid this situation we want to have only one call to
|
||||
// native handler registration (for the same element and same type of event).
|
||||
//
|
||||
// In order to have just one native event handler in presence of multiple handler functions,
|
||||
// we just register a first handler function as a native event listener and then chain
|
||||
// (coalesce) other handler functions on top of the first native handler function.
|
||||
let existingListener = null;
|
||||
// Please note that the coalescing described here doesn't happen for events specifying an
|
||||
// alternative target (ex. (document:click)) - this is to keep backward compatibility with the
|
||||
// view engine.
|
||||
// Also, we don't have to search for existing listeners is there are no directives
|
||||
// matching on a given node as we can't register multiple event handlers for the same event in
|
||||
// a template (this would mean having duplicate attributes).
|
||||
if (!eventTargetResolver && isTNodeDirectiveHost) {
|
||||
existingListener = findExistingListener(tView, lView, eventName, tNode.index);
|
||||
}
|
||||
if (existingListener !== null) {
|
||||
// Attach a new listener to coalesced listeners list, maintaining the order in which
|
||||
// listeners are registered. For performance reasons, we keep a reference to the last
|
||||
// listener in that list (in `__ngLastListenerFn__` field), so we can avoid going through
|
||||
// the entire set each time we need to add a new listener.
|
||||
const lastListenerFn = (<any>existingListener).__ngLastListenerFn__ || existingListener;
|
||||
lastListenerFn.__ngNextListenerFn__ = listenerFn;
|
||||
(<any>existingListener).__ngLastListenerFn__ = listenerFn;
|
||||
if (hasCoalescedDomEvent) {
|
||||
processOutputs = false;
|
||||
} else {
|
||||
listenerFn = wrapListener(tNode, lView, listenerFn);
|
||||
stashEventListener(target as RElement, eventName, listenerFn);
|
||||
const cleanupFn = renderer.listen(target as RElement, eventName, listenerFn);
|
||||
|
||||
lCleanup.push(listenerFn, cleanupFn);
|
||||
tCleanup && tCleanup.push(eventName, idxOrTargetGetter, lCleanupIndex, lCleanupIndex + 1);
|
||||
}
|
||||
} else {
|
||||
// Even if there is no native listener to add, we still need to wrap the listener so that OnPush
|
||||
// ancestors are marked dirty when an event occurs.
|
||||
listenerFn = wrapListener(tNode, lView, listenerFn);
|
||||
}
|
||||
|
||||
if (processOutputs) {
|
||||
@@ -228,13 +137,15 @@ export function listenerInternal(
|
||||
for (let i = 0; i < hostDirectiveOutputConfig.length; i += 2) {
|
||||
const index = hostDirectiveOutputConfig[i] as number;
|
||||
const lookupName = hostDirectiveOutputConfig[i + 1] as string;
|
||||
listenToOutput(tNode, lView, index, lookupName, eventName, listenerFn);
|
||||
wrappedListener ??= wrapListener(tNode, lView, listenerFn);
|
||||
listenToOutput(tNode, lView, index, lookupName, eventName, wrappedListener);
|
||||
}
|
||||
}
|
||||
|
||||
if (outputConfig && outputConfig.length) {
|
||||
for (const index of outputConfig) {
|
||||
listenToOutput(tNode, lView, index, eventName, eventName, listenerFn);
|
||||
wrappedListener ??= wrapListener(tNode, lView, listenerFn);
|
||||
listenToOutput(tNode, lView, index, eventName, eventName, wrappedListener);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,13 +10,27 @@ import {setActiveConsumer} from '@angular/core/primitives/signals';
|
||||
|
||||
import {NotificationSource} from '../../change_detection/scheduling/zoneless_scheduling';
|
||||
import {TNode} from '../interfaces/node';
|
||||
import {isComponentHost} from '../interfaces/type_checks';
|
||||
import {CONTEXT, INJECTOR, LView} from '../interfaces/view';
|
||||
import {getComponentLViewByIndex} from '../util/view_utils';
|
||||
import {isComponentHost, isDirectiveHost} from '../interfaces/type_checks';
|
||||
import {CLEANUP, CONTEXT, INJECTOR, LView, TView} from '../interfaces/view';
|
||||
import {getComponentLViewByIndex, getNativeByTNode, unwrapRNode} from '../util/view_utils';
|
||||
import {profiler} from '../profiler';
|
||||
import {ProfilerEvent} from '../profiler_types';
|
||||
import {ErrorHandler} from '../../error_handler';
|
||||
import {markViewDirty} from '../instructions/mark_view_dirty';
|
||||
import {RElement, RNode} from '../interfaces/renderer_dom';
|
||||
import {GlobalTargetResolver, Renderer} from '../interfaces/renderer';
|
||||
|
||||
/**
|
||||
* Contains a reference to a function that disables event replay feature
|
||||
* for server-side rendered applications. This function is overridden with
|
||||
* an actual implementation when the event replay feature is enabled via
|
||||
* `withEventReplay()` call.
|
||||
*/
|
||||
let stashEventListener = (el: RNode, eventName: string, listenerFn: (e?: any) => any) => {};
|
||||
|
||||
export function setStashFn(fn: typeof stashEventListener) {
|
||||
stashEventListener = fn;
|
||||
}
|
||||
|
||||
/**
|
||||
* Wraps an event listener with a function that marks ancestors dirty and prevents default behavior,
|
||||
@@ -88,3 +102,101 @@ function handleError(lView: LView, error: any): void {
|
||||
}
|
||||
injector.get(ErrorHandler, null)?.handleError(error);
|
||||
}
|
||||
|
||||
export function listenToDomEvent(
|
||||
tNode: TNode,
|
||||
lView: LView<{} | null>,
|
||||
eventTargetResolver: GlobalTargetResolver | undefined,
|
||||
lCleanup: any[],
|
||||
tView: TView,
|
||||
eventName: string,
|
||||
wrappedListener: (e?: any) => any,
|
||||
originalListener: (e?: any) => any,
|
||||
renderer: Renderer,
|
||||
tCleanup: any[] | null,
|
||||
) {
|
||||
const isTNodeDirectiveHost = isDirectiveHost(tNode);
|
||||
const native = getNativeByTNode(tNode, lView) as RElement;
|
||||
const target = eventTargetResolver ? eventTargetResolver(native) : native;
|
||||
const lCleanupIndex = lCleanup.length;
|
||||
const idxOrTargetGetter = eventTargetResolver
|
||||
? (_lView: LView) => eventTargetResolver(unwrapRNode(_lView[tNode.index]))
|
||||
: tNode.index;
|
||||
let hasCoalesced = false;
|
||||
|
||||
// In order to match current behavior, native DOM event listeners must be added for all
|
||||
// events (including outputs).
|
||||
// There might be cases where multiple directives on the same element try to register an event
|
||||
// handler function for the same event. In this situation we want to avoid registration of
|
||||
// several native listeners as each registration would be intercepted by NgZone and
|
||||
// trigger change detection. This would mean that a single user action would result in several
|
||||
// change detections being invoked. To avoid this situation we want to have only one call to
|
||||
// native handler registration (for the same element and same type of event).
|
||||
//
|
||||
// In order to have just one native event handler in presence of multiple handler functions,
|
||||
// we just register a first handler function as a native event listener and then chain
|
||||
// (coalesce) other handler functions on top of the first native handler function.
|
||||
let existingListener: any = null;
|
||||
// Please note that the coalescing described here doesn't happen for events specifying an
|
||||
// alternative target (ex. (document:click)) - this is to keep backward compatibility with the
|
||||
// view engine.
|
||||
// Also, we don't have to search for existing listeners if there are no directives
|
||||
// matching on a given node as we can't register multiple event handlers for the same event in
|
||||
// a template (this would mean having duplicate attributes).
|
||||
if (!eventTargetResolver && isTNodeDirectiveHost) {
|
||||
existingListener = findExistingListener(tView, lView, eventName, tNode.index);
|
||||
}
|
||||
if (existingListener !== null) {
|
||||
// Attach a new listener to coalesced listeners list, maintaining the order in which
|
||||
// listeners are registered. For performance reasons, we keep a reference to the last
|
||||
// listener in that list (in `__ngLastListenerFn__` field), so we can avoid going through
|
||||
// the entire set each time we need to add a new listener.
|
||||
const lastListenerFn = existingListener.__ngLastListenerFn__ || existingListener;
|
||||
lastListenerFn.__ngNextListenerFn__ = originalListener;
|
||||
existingListener.__ngLastListenerFn__ = originalListener;
|
||||
hasCoalesced = true;
|
||||
} else {
|
||||
stashEventListener(target as RElement, eventName, wrappedListener);
|
||||
const cleanupFn = renderer.listen(target as RElement, eventName, wrappedListener);
|
||||
|
||||
lCleanup.push(wrappedListener, cleanupFn);
|
||||
tCleanup && tCleanup.push(eventName, idxOrTargetGetter, lCleanupIndex, lCleanupIndex + 1);
|
||||
}
|
||||
return hasCoalesced;
|
||||
}
|
||||
|
||||
/**
|
||||
* A utility function that checks if a given element has already an event handler registered for an
|
||||
* event with a specified name. The TView.cleanup data structure is used to find out which events
|
||||
* are registered for a given element.
|
||||
*/
|
||||
function findExistingListener(
|
||||
tView: TView,
|
||||
lView: LView,
|
||||
eventName: string,
|
||||
tNodeIndex: number,
|
||||
): ((e?: any) => any) | null {
|
||||
const tCleanup = tView.cleanup;
|
||||
if (tCleanup != null) {
|
||||
for (let i = 0; i < tCleanup.length - 1; i += 2) {
|
||||
const cleanupEventName = tCleanup[i];
|
||||
if (cleanupEventName === eventName && tCleanup[i + 1] === tNodeIndex) {
|
||||
// We have found a matching event name on the same node but it might not have been
|
||||
// registered yet, so we must explicitly verify entries in the LView cleanup data
|
||||
// structures.
|
||||
const lCleanup = lView[CLEANUP]!;
|
||||
const listenerIdxInLCleanup = tCleanup[i + 2];
|
||||
return lCleanup.length > listenerIdxInLCleanup ? lCleanup[listenerIdxInLCleanup] : null;
|
||||
}
|
||||
// TView.cleanup can have a mix of 4-elements entries (for event handler cleanups) or
|
||||
// 2-element entries (for directive and queries destroy hooks). As such we can encounter
|
||||
// blocks of 4 or 2 items in the tView.cleanup and this is why we iterate over 2 elements
|
||||
// first and jump another 2 elements if we detect listeners cleanup (4 elements). Also check
|
||||
// documentation of TView.cleanup for more details of this data structure layout.
|
||||
if (typeof cleanupEventName === 'string') {
|
||||
i += 2;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user