Skip to content

Commit c1004cb

Browse files
crisbetotinayuangao
authored andcommitted
perf(scroll-dispatcher): lazily subscribe to global events (#3270)
* perf(scroll-dispatcher): lazily subscribe to global events Switches the `ScrollDispatcher` to only subscribe to `scroll` and `resize` events if there any registered callbacks. Also unsubscribes once there are no more callbacks. Fixes #3237. * refactor: account for the `scrolled` subscriptions when adding the global listeners * fix: wrong value being passed in * fix: don't add global listener for scrollables
1 parent d78a370 commit c1004cb

File tree

4 files changed

+59
-16
lines changed

4 files changed

+59
-16
lines changed

src/lib/core/overlay/position/viewport-ruler.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ export class ViewportRuler {
1717
this._cacheViewportGeometry();
1818

1919
// Subscribe to scroll and resize events and update the document rectangle on changes.
20-
scrollDispatcher.scrolled().subscribe(() => this._cacheViewportGeometry());
20+
scrollDispatcher.scrolled(null, () => this._cacheViewportGeometry());
2121
}
2222

2323
/** Gets a ClientRect for the viewport's bounds. */

src/lib/core/overlay/scroll/scroll-dispatcher.spec.ts

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ describe('Scroll Dispatcher', () => {
4949
// Listen for notifications from scroll service with a throttle of 100ms
5050
const throttleTime = 100;
5151
let hasServiceScrollNotified = false;
52-
scroll.scrolled(throttleTime).subscribe(() => { hasServiceScrollNotified = true; });
52+
scroll.scrolled(throttleTime, () => { hasServiceScrollNotified = true; });
5353

5454
// Emit a scroll event from the scrolling element in our component.
5555
// This event should be picked up by the scrollable directive and notify.
@@ -89,6 +89,29 @@ describe('Scroll Dispatcher', () => {
8989
expect(scrollableElementIds).toEqual(['scrollable-1', 'scrollable-1a']);
9090
});
9191
});
92+
93+
describe('lazy subscription', () => {
94+
let scroll: ScrollDispatcher;
95+
96+
beforeEach(inject([ScrollDispatcher], (s: ScrollDispatcher) => {
97+
scroll = s;
98+
}));
99+
100+
it('should lazily add global listeners as service subscriptions are added and removed', () => {
101+
expect(scroll._globalSubscription).toBeNull('Expected no global listeners on init.');
102+
103+
const subscription = scroll.scrolled(0, () => {});
104+
105+
expect(scroll._globalSubscription).toBeTruthy(
106+
'Expected global listeners after a subscription has been added.');
107+
108+
subscription.unsubscribe();
109+
110+
expect(scroll._globalSubscription).toBeNull(
111+
'Expected global listeners to have been removed after the subscription has stopped.');
112+
});
113+
114+
});
92115
});
93116

94117

src/lib/core/overlay/scroll/scroll-dispatcher.ts

Lines changed: 33 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import {Subject} from 'rxjs/Subject';
44
import {Observable} from 'rxjs/Observable';
55
import {Subscription} from 'rxjs/Subscription';
66
import 'rxjs/add/observable/fromEvent';
7+
import 'rxjs/add/observable/merge';
78
import 'rxjs/add/operator/auditTime';
89

910

@@ -19,25 +20,26 @@ export class ScrollDispatcher {
1920
/** Subject for notifying that a registered scrollable reference element has been scrolled. */
2021
_scrolled: Subject<void> = new Subject<void>();
2122

23+
/** Keeps track of the global `scroll` and `resize` subscriptions. */
24+
_globalSubscription: Subscription = null;
25+
26+
/** Keeps track of the amount of subscriptions to `scrolled`. Used for cleaning up afterwards. */
27+
private _scrolledCount = 0;
28+
2229
/**
2330
* Map of all the scrollable references that are registered with the service and their
2431
* scroll event subscriptions.
2532
*/
2633
scrollableReferences: Map<Scrollable, Subscription> = new Map();
2734

28-
constructor() {
29-
// By default, notify a scroll event when the document is scrolled or the window is resized.
30-
Observable.fromEvent(window.document, 'scroll').subscribe(() => this._notify());
31-
Observable.fromEvent(window, 'resize').subscribe(() => this._notify());
32-
}
33-
3435
/**
3536
* Registers a Scrollable with the service and listens for its scrolled events. When the
3637
* scrollable is scrolled, the service emits the event in its scrolled observable.
3738
* @param scrollable Scrollable instance to be registered.
3839
*/
3940
register(scrollable: Scrollable): void {
4041
const scrollSubscription = scrollable.elementScrolled().subscribe(() => this._notify());
42+
4143
this.scrollableReferences.set(scrollable, scrollSubscription);
4244
}
4345

@@ -53,18 +55,36 @@ export class ScrollDispatcher {
5355
}
5456

5557
/**
56-
* Returns an observable that emits an event whenever any of the registered Scrollable
58+
* Subscribes to an observable that emits an event whenever any of the registered Scrollable
5759
* references (or window, document, or body) fire a scrolled event. Can provide a time in ms
5860
* to override the default "throttle" time.
5961
*/
60-
scrolled(auditTimeInMs: number = DEFAULT_SCROLL_TIME): Observable<void> {
61-
// In the case of a 0ms delay, return the observable without auditTime since it does add
62-
// a perceptible delay in processing overhead.
63-
if (auditTimeInMs == 0) {
64-
return this._scrolled.asObservable();
62+
scrolled(auditTimeInMs: number = DEFAULT_SCROLL_TIME, callback: () => any): Subscription {
63+
// In the case of a 0ms delay, use an observable without auditTime
64+
// since it does add a perceptible delay in processing overhead.
65+
let observable = auditTimeInMs > 0 ?
66+
this._scrolled.asObservable().auditTime(auditTimeInMs) :
67+
this._scrolled.asObservable();
68+
69+
this._scrolledCount++;
70+
71+
if (!this._globalSubscription) {
72+
this._globalSubscription = Observable.merge(
73+
Observable.fromEvent(window.document, 'scroll'),
74+
Observable.fromEvent(window, 'resize')
75+
).subscribe(() => this._notify());
6576
}
6677

67-
return this._scrolled.asObservable().auditTime(auditTimeInMs);
78+
// Note that we need to do the subscribing from here, in order to be able to remove
79+
// the global event listeners once there are no more subscriptions.
80+
return observable.subscribe(callback).add(() => {
81+
this._scrolledCount--;
82+
83+
if (this._globalSubscription && !this.scrollableReferences.size && !this._scrolledCount) {
84+
this._globalSubscription.unsubscribe();
85+
this._globalSubscription = null;
86+
}
87+
});
6888
}
6989

7090
/** Returns all registered Scrollables that contain the provided element. */

src/lib/tooltip/tooltip.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -146,7 +146,7 @@ export class MdTooltip implements OnInit, OnDestroy {
146146
ngOnInit() {
147147
// When a scroll on the page occurs, update the position in case this tooltip needs
148148
// to be repositioned.
149-
this.scrollSubscription = this._scrollDispatcher.scrolled(SCROLL_THROTTLE_MS).subscribe(() => {
149+
this.scrollSubscription = this._scrollDispatcher.scrolled(SCROLL_THROTTLE_MS, () => {
150150
if (this._overlayRef) {
151151
this._overlayRef.updatePosition();
152152
}

0 commit comments

Comments
 (0)