refactor(core): Simplify event handler extraction logic. (#55747)

This reuses information already recorded during hydration to
remove jsaction attributes to also stash event handlers. This avoids
a tree walk and looku.

PR Close #55747
This commit is contained in:
Thomas Nguyen
2024-05-10 09:45:06 -07:00
committed by Andrew Scott
parent 89e860cc30
commit bfb5f2ba59
2 changed files with 25 additions and 79 deletions
+22 -76
View File
@@ -20,11 +20,10 @@ import {APP_ID} from '../application/application_tokens';
import {ENVIRONMENT_INITIALIZER, Injector} from '../di';
import {inject} from '../di/injector_compatibility';
import {Provider} from '../di/interface/provider';
import {attachLViewId, readLView} from '../render3/context_discovery';
import {setDisableEventReplayImpl} from '../render3/instructions/listener';
import {TNode, TNodeType} from '../render3/interfaces/node';
import {RElement, RNode} from '../render3/interfaces/renderer_dom';
import {CLEANUP, LView, TVIEW, TView} from '../render3/interfaces/view';
import {RNode} from '../render3/interfaces/renderer_dom';
import {CLEANUP, LView, TView} from '../render3/interfaces/view';
import {isPlatformBrowser} from '../render3/util/misc_utils';
import {unwrapRNode} from '../render3/util/view_utils';
@@ -43,7 +42,7 @@ function getJsactionData(container: EarlyJsactionDataContainer) {
}
const JSACTION_ATTRIBUTE = 'jsaction';
const removeJsactionQueue: RElement[] = [];
const jsactionMap: Map<Element, Map<string, Function[]>> = new Map();
/**
* Returns a set of providers required to setup support for event replay.
@@ -58,11 +57,18 @@ export function withEventReplay(): Provider[] {
{
provide: ENVIRONMENT_INITIALIZER,
useValue: () => {
setDisableEventReplayImpl((el: RElement) => {
if (el.hasAttribute(JSACTION_ATTRIBUTE)) {
setDisableEventReplayImpl((rEl, eventName, listenerFn) => {
if (rEl.hasAttribute(JSACTION_ATTRIBUTE)) {
const el = unwrapRNode(rEl) as Element;
// We don't immediately remove the attribute here because
// we need it for replay that happens after hydration.
removeJsactionQueue.push(el);
if (!jsactionMap.has(el)) {
jsactionMap.set(el, new Map());
}
if (!jsactionMap.get(el)!.has(eventName)) {
jsactionMap.get(el)!.set(eventName, []);
}
jsactionMap.get(el)!.get(eventName)!.push(listenerFn);
}
});
},
@@ -101,14 +107,14 @@ export function withEventReplay(): Provider[] {
for (const event of queue) {
handleEvent(event);
}
jsactionMap.clear();
queue.length = 0;
},
});
registerDispatcher(eventContract, dispatcher);
for (const el of removeJsactionQueue) {
for (const el of jsactionMap.keys()) {
el.removeAttribute(JSACTION_ATTRIBUTE);
}
removeJsactionQueue.length = 0;
}
});
};
@@ -185,73 +191,13 @@ export function setJSActionAttribute(
}
}
/**
* Finds an LView that a given DOM element belongs to.
*/
function getLViewByElement(target: HTMLElement): LView | null {
let lView = readLView(target);
if (lView) {
return lView;
} else {
// If this node doesn't have LView info attached, then we need to
// traverse upwards up the DOM to find the nearest element that
// has already been monkey patched with data.
let parent = target as HTMLElement;
while ((parent = parent.parentNode as HTMLElement)) {
lView = readLView(parent);
if (lView) {
// To prevent additional lookups, monkey-patch LView id onto this DOM node.
// TODO: consider patching all parent nodes that didn't have LView id, so that
// we can avoid lookups for more nodes.
attachLViewId(target, lView);
return lView;
}
}
}
return null;
}
function handleEvent(event: EventInfoWrapper) {
const nativeElement = event.getAction()!.element;
// Dispatch event via Angular's logic
if (nativeElement) {
const lView = getLViewByElement(nativeElement as HTMLElement);
if (lView !== null) {
const tView = lView[TVIEW];
const eventName = event.getEventType();
const origEvent = event.getEvent();
const listeners = getEventListeners(tView, lView, nativeElement, eventName);
for (const listener of listeners) {
listener(origEvent);
}
}
const nativeElement = unwrapRNode(event.getAction()!.element) as Element;
const handlerFns = jsactionMap.get(nativeElement)?.get(event.getEventType());
if (!handlerFns) {
return;
}
for (const handler of handlerFns) {
handler(event.getEvent());
}
}
type Listener = ((value: Event) => unknown) | (() => unknown);
function getEventListeners(
tView: TView,
lView: LView,
nativeElement: Element,
eventName: string,
): Listener[] {
const listeners: Listener[] = [];
const lCleanup = lView[CLEANUP];
const tCleanup = tView.cleanup;
if (tCleanup && lCleanup) {
for (let i = 0; i < tCleanup.length; ) {
const storedEventName = tCleanup[i++];
const nativeElementIndex = tCleanup[i++];
if (typeof storedEventName === 'string') {
const listenerElement = unwrapRNode(lView[nativeElementIndex]) as any as Element;
const listener: Listener = lCleanup[tCleanup[i++]];
i++; // increment to the next position;
if (listenerElement === nativeElement && eventName === storedEventName) {
listeners.push(listener);
}
}
}
}
return listeners;
}
@@ -34,9 +34,9 @@ import {
* an actual implementation when the event replay feature is enabled via
* `withEventReplay()` call.
*/
let disableEventReplayFn = (el: RElement) => {};
let disableEventReplayFn = (el: RElement, eventName: string, listenerFn: (e?: any) => any) => {};
export function setDisableEventReplayImpl(fn: (el: RElement) => void) {
export function setDisableEventReplayImpl(fn: typeof disableEventReplayFn) {
disableEventReplayFn = fn;
}
@@ -181,7 +181,7 @@ export function listenerInternal(
? (_lView: LView) => eventTargetResolver(unwrapRNode(_lView[tNode.index]))
: tNode.index;
disableEventReplayFn(native);
disableEventReplayFn(native, eventName, listenerFn);
// In order to match current behavior, native DOM event listeners must be added for all
// events (including outputs).