Skip to content

Commit

Permalink
refactor(core): Simplify event handler extraction logic. (#55747)
Browse files Browse the repository at this point in the history
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
  • Loading branch information
iteriani authored and atscott committed May 14, 2024
1 parent 89e860c commit bfb5f2b
Show file tree
Hide file tree
Showing 2 changed files with 24 additions and 78 deletions.
96 changes: 21 additions & 75 deletions packages/core/src/hydration/event_replay.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand All @@ -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.
Expand All @@ -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);
}
});
},
Expand Down Expand Up @@ -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;
}
});
};
Expand Down Expand Up @@ -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;
}
}

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);
}
}
}
for (const handler of handlerFns) {
handler(event.getEvent());
}
return listeners;
}
6 changes: 3 additions & 3 deletions packages/core/src/render3/instructions/listener.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand Down Expand Up @@ -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).
Expand Down

0 comments on commit bfb5f2b

Please sign in to comment.