Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

perf(material/core): delegate trigger events #26147

Merged
merged 1 commit into from Dec 2, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
81 changes: 81 additions & 0 deletions src/material/core/ripple/ripple-event-manager.ts
@@ -0,0 +1,81 @@
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/

import {normalizePassiveListenerOptions, _getEventTarget} from '@angular/cdk/platform';
import {NgZone} from '@angular/core';

/** Options used to bind a passive capturing event. */
const passiveCapturingEventOptions = normalizePassiveListenerOptions({
passive: true,
capture: true,
});

/** Manages events through delegation so that as few event handlers as possible are bound. */
export class RippleEventManager {
private _events = new Map<string, Map<HTMLElement, Set<EventListenerObject>>>();

/** Adds an event handler. */
addHandler(ngZone: NgZone, name: string, element: HTMLElement, handler: EventListenerObject) {
const handlersForEvent = this._events.get(name);

if (handlersForEvent) {
const handlersForElement = handlersForEvent.get(element);

if (handlersForElement) {
handlersForElement.add(handler);
} else {
handlersForEvent.set(element, new Set([handler]));
}
} else {
this._events.set(name, new Map([[element, new Set([handler])]]));

ngZone.runOutsideAngular(() => {
document.addEventListener(name, this._delegateEventHandler, passiveCapturingEventOptions);
});
}
}

/** Removes an event handler. */
removeHandler(name: string, element: HTMLElement, handler: EventListenerObject) {
const handlersForEvent = this._events.get(name);

if (!handlersForEvent) {
return;
}

const handlersForElement = handlersForEvent.get(element);

if (!handlersForElement) {
return;
}

handlersForElement.delete(handler);

if (handlersForElement.size === 0) {
handlersForEvent.delete(element);
}

if (handlersForEvent.size === 0) {
this._events.delete(name);
document.removeEventListener(name, this._delegateEventHandler, passiveCapturingEventOptions);
}
}

/** Event handler that is bound and which dispatches the events to the different targets. */
private _delegateEventHandler = (event: Event) => {
const target = _getEventTarget(event);

if (target) {
this._events.get(event.type)?.forEach((handlers, element) => {
if (element === target || element.contains(target as Node)) {
handlers.forEach(handler => handler.handleEvent(event));
}
});
}
};
}
63 changes: 38 additions & 25 deletions src/material/core/ripple/ripple-renderer.ts
Expand Up @@ -6,10 +6,11 @@
* found in the LICENSE file at https://angular.io/license
*/
import {ElementRef, NgZone} from '@angular/core';
import {Platform, normalizePassiveListenerOptions} from '@angular/cdk/platform';
import {Platform, normalizePassiveListenerOptions, _getEventTarget} from '@angular/cdk/platform';
import {isFakeMousedownFromScreenReader, isFakeTouchstartFromScreenReader} from '@angular/cdk/a11y';
import {coerceElement} from '@angular/cdk/coercion';
import {RippleRef, RippleState, RippleConfig} from './ripple-ref';
import {RippleEventManager} from './ripple-event-manager';

/**
* Interface that describes the target for launching ripples.
Expand Down Expand Up @@ -45,8 +46,11 @@ export const defaultRippleAnimationConfig = {
*/
const ignoreMouseEventsTimeout = 800;

/** Options that apply to all the event listeners that are bound by the ripple renderer. */
const passiveEventOptions = normalizePassiveListenerOptions({passive: true});
/** Options used to bind a passive capturing event. */
const passiveCapturingEventOptions = normalizePassiveListenerOptions({
passive: true,
capture: true,
});

/** Events that signal that the pointer is down. */
const pointerDownEvents = ['mousedown', 'touchstart'];
Expand Down Expand Up @@ -94,14 +98,16 @@ export class RippleRenderer implements EventListenerObject {
*/
private _containerRect: ClientRect | null;

private static _eventManager = new RippleEventManager();

constructor(
private _target: RippleTarget,
private _ngZone: NgZone,
elementOrElementRef: HTMLElement | ElementRef<HTMLElement>,
platform: Platform,
private _platform: Platform,
) {
// Only do anything if we're on the browser.
if (platform.isBrowser) {
if (_platform.isBrowser) {
this._containerElement = coerceElement(elementOrElementRef);
}
}
Expand Down Expand Up @@ -252,15 +258,19 @@ export class RippleRenderer implements EventListenerObject {
setupTriggerEvents(elementOrElementRef: HTMLElement | ElementRef<HTMLElement>) {
const element = coerceElement(elementOrElementRef);

if (!element || element === this._triggerElement) {
if (!this._platform.isBrowser || !element || element === this._triggerElement) {
return;
}

// Remove all previously registered event listeners from the trigger element.
this._removeTriggerEvents();

this._triggerElement = element;
this._registerEvents(pointerDownEvents);

// Use event delegation for the trigger events since they're
// set up during creation and are performance-sensitive.
pointerDownEvents.forEach(type => {
RippleRenderer._eventManager.addHandler(this._ngZone, type, element, this);
});
}

/**
Expand All @@ -280,7 +290,17 @@ export class RippleRenderer implements EventListenerObject {
// We do this on-demand in order to reduce the total number of event listeners
// registered by the ripples, which speeds up the rendering time for large UIs.
if (!this._pointerUpEventsRegistered) {
this._registerEvents(pointerUpEvents);
// The events for hiding the ripple are bound directly on the trigger, because:
// 1. Some of them occur frequently (e.g. `mouseleave`) and any advantage we get from
// delegation will be diminished by having to look through all the data structures often.
// 2. They aren't as performance-sensitive, because they're bound only after the user
// has interacted with an element.
this._ngZone.runOutsideAngular(() => {
pointerUpEvents.forEach(type => {
this._triggerElement!.addEventListener(type, this, passiveCapturingEventOptions);
});
});

this._pointerUpEventsRegistered = true;
}
}
Expand Down Expand Up @@ -393,30 +413,23 @@ export class RippleRenderer implements EventListenerObject {
});
}

/** Registers event listeners for a given list of events. */
private _registerEvents(eventTypes: string[]) {
this._ngZone.runOutsideAngular(() => {
eventTypes.forEach(type => {
this._triggerElement!.addEventListener(type, this, passiveEventOptions);
});
});
}

private _getActiveRipples(): RippleRef[] {
return Array.from(this._activeRipples.keys());
}

/** Removes previously registered event listeners from the trigger element. */
_removeTriggerEvents() {
if (this._triggerElement) {
pointerDownEvents.forEach(type => {
this._triggerElement!.removeEventListener(type, this, passiveEventOptions);
});
const trigger = this._triggerElement;

if (trigger) {
pointerDownEvents.forEach(type =>
RippleRenderer._eventManager.removeHandler(type, trigger, this),
);

if (this._pointerUpEventsRegistered) {
pointerUpEvents.forEach(type => {
this._triggerElement!.removeEventListener(type, this, passiveEventOptions);
});
pointerUpEvents.forEach(type =>
trigger.removeEventListener(type, this, passiveCapturingEventOptions),
);
}
}
}
Expand Down
2 changes: 1 addition & 1 deletion tools/public_api_guard/material/core.md
Expand Up @@ -524,7 +524,7 @@ export class RippleRef {

// @public
export class RippleRenderer implements EventListenerObject {
constructor(_target: RippleTarget, _ngZone: NgZone, elementOrElementRef: HTMLElement | ElementRef<HTMLElement>, platform: Platform);
constructor(_target: RippleTarget, _ngZone: NgZone, elementOrElementRef: HTMLElement | ElementRef<HTMLElement>, _platform: Platform);
fadeInRipple(x: number, y: number, config?: RippleConfig): RippleRef;
fadeOutAll(): void;
fadeOutAllNonPersistent(): void;
Expand Down