From 9f2b2e7324dcdac278cb70e889dcf7a770a3217b Mon Sep 17 00:00:00 2001 From: crisbeto Date: Fri, 25 Jan 2019 22:39:57 +0100 Subject: [PATCH] perf(focus-monitor): avoid triggering change detection if there are no subscribers to stream Currently we have an `NgZone.run` call on each `focus` and `blur` event of a monitored event in order to bring its subscribers into the `NgZone`, however this means that we're also triggering change detection to any consumers that aren't subscribed to changes (e.g. `mat-button` only cares about the classes being applied). These changes move around some logic so that the `NgZone.run` is only hit if somebody has subscribed to the observable. --- src/cdk/a11y/focus-monitor/focus-monitor.ts | 38 +++++++++++++-------- 1 file changed, 24 insertions(+), 14 deletions(-) diff --git a/src/cdk/a11y/focus-monitor/focus-monitor.ts b/src/cdk/a11y/focus-monitor/focus-monitor.ts index 58f207e2a47f..e7befafe4422 100644 --- a/src/cdk/a11y/focus-monitor/focus-monitor.ts +++ b/src/cdk/a11y/focus-monitor/focus-monitor.ts @@ -18,7 +18,7 @@ import { Output, SkipSelf, } from '@angular/core'; -import {Observable, of as observableOf, Subject, Subscription} from 'rxjs'; +import {Observable, of as observableOf, Subject, Subscription, Observer} from 'rxjs'; import {coerceElement} from '@angular/cdk/coercion'; @@ -41,7 +41,8 @@ export interface FocusOptions { type MonitoredElementInfo = { unlisten: Function, checkChildren: boolean, - subject: Subject + subject: Subject, + observable: Observable }; /** @@ -169,17 +170,30 @@ export class FocusMonitor implements OnDestroy { } // Create monitored element info. - let info: MonitoredElementInfo = { + const subject = new Subject(); + const info: MonitoredElementInfo = { unlisten: () => {}, - checkChildren: checkChildren, - subject: new Subject() + checkChildren, + subject, + // Note that we want the observable to emit inside the NgZone, however we don't want to + // trigger change detection if nobody has subscribed to it. We do so by creating the + // observable manually. + observable: new Observable((observer: Observer) => { + const subscription = subject.subscribe(origin => { + this._ngZone.run(() => observer.next(origin)); + }); + + return () => { + subscription.unsubscribe(); + }; + }) }; this._elementInfo.set(nativeElement, info); this._incrementMonitoredElementCount(); // Start listening. We need to listen in capture phase since focus events don't bubble. - let focusListener = (event: FocusEvent) => this._onFocus(event, nativeElement); - let blurListener = (event: FocusEvent) => this._onBlur(event, nativeElement); + const focusListener = (event: FocusEvent) => this._onFocus(event, nativeElement); + const blurListener = (event: FocusEvent) => this._onBlur(event, nativeElement); this._ngZone.runOutsideAngular(() => { nativeElement.addEventListener('focus', focusListener, true); nativeElement.addEventListener('blur', blurListener, true); @@ -191,7 +205,7 @@ export class FocusMonitor implements OnDestroy { nativeElement.removeEventListener('blur', blurListener, true); }; - return info.subject.asObservable(); + return info.observable; } /** @@ -358,7 +372,7 @@ export class FocusMonitor implements OnDestroy { } this._setClasses(element, origin); - this._emitOrigin(elementInfo.subject, origin); + elementInfo.subject.next(origin); this._lastFocusOrigin = origin; } @@ -378,11 +392,7 @@ export class FocusMonitor implements OnDestroy { } this._setClasses(element); - this._emitOrigin(elementInfo.subject, null); - } - - private _emitOrigin(subject: Subject, origin: FocusOrigin) { - this._ngZone.run(() => subject.next(origin)); + elementInfo.subject.next(null); } private _incrementMonitoredElementCount() {