Skip to content

Commit 42e7cfb

Browse files
committed
perf(scroll-dispatcher): avoid triggering change detection on scroll
* Avoids triggering change detection when listening to global scroll events in the `ScrollDispatcher`. * Avoids triggering change detection when scrolling inside of `Scrollable` instances. * Switches a `ScrollDispatcher` test to use spies, instead of toggling booleans.
1 parent 259ff5f commit 42e7cfb

File tree

5 files changed

+67
-21
lines changed

5 files changed

+67
-21
lines changed

src/lib/core/overlay/position/connected-position-strategy.spec.ts

+3-1
Original file line numberDiff line numberDiff line change
@@ -534,7 +534,9 @@ describe('ConnectedPositionStrategy', () => {
534534
fakeElementRef,
535535
{originX: 'start', originY: 'bottom'},
536536
{overlayX: 'start', overlayY: 'top'});
537-
strategy.withScrollableContainers([new Scrollable(new FakeElementRef(scrollable), null)]);
537+
538+
strategy.withScrollableContainers([
539+
new Scrollable(new FakeElementRef(scrollable), null, null, null)]);
538540

539541
positionChangeHandler = jasmine.createSpy('positionChangeHandler');
540542
onPositionChangeSubscription = strategy.onPositionChange.subscribe(positionChangeHandler);

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

+30-8
Original file line numberDiff line numberDiff line change
@@ -41,32 +41,54 @@ describe('Scroll Dispatcher', () => {
4141

4242
it('should notify through the directive and service that a scroll event occurred',
4343
fakeAsync(() => {
44-
let hasDirectiveScrollNotified = false;
4544
// Listen for notifications from scroll directive
46-
let scrollable = fixture.componentInstance.scrollable;
47-
scrollable.elementScrolled().subscribe(() => { hasDirectiveScrollNotified = true; });
45+
const scrollable = fixture.componentInstance.scrollable;
46+
const directiveSpy = jasmine.createSpy('directive scroll callback');
47+
scrollable.elementScrolled().subscribe(directiveSpy);
4848

4949
// Listen for notifications from scroll service with a throttle of 100ms
5050
const throttleTime = 100;
51-
let hasServiceScrollNotified = false;
52-
scroll.scrolled(throttleTime, () => { hasServiceScrollNotified = true; });
51+
const serviceSpy = jasmine.createSpy('service scroll callback');
52+
scroll.scrolled(throttleTime, serviceSpy);
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.
5656
// The notification should be picked up by the service.
5757
dispatchFakeEvent(fixture.componentInstance.scrollingElement.nativeElement, 'scroll');
5858

5959
// The scrollable directive should have notified the service immediately.
60-
expect(hasDirectiveScrollNotified).toBe(true);
60+
expect(directiveSpy).toHaveBeenCalled();
6161

6262
// Verify that the throttle is used, the service should wait for the throttle time until
6363
// sending the notification.
64-
expect(hasServiceScrollNotified).toBe(false);
64+
expect(serviceSpy).not.toHaveBeenCalled();
6565

6666
// After the throttle time, the notification should be sent.
6767
tick(throttleTime);
68-
expect(hasServiceScrollNotified).toBe(true);
68+
expect(serviceSpy).toHaveBeenCalled();
6969
}));
70+
71+
it('should not execute the global events in the Angular zone', () => {
72+
const spy = jasmine.createSpy('zone unstable callback');
73+
const subscription = fixture.ngZone.onUnstable.subscribe(spy);
74+
75+
scroll.scrolled(0, () => {});
76+
dispatchFakeEvent(document, 'scroll');
77+
dispatchFakeEvent(window, 'resize');
78+
79+
expect(spy).not.toHaveBeenCalled();
80+
subscription.unsubscribe();
81+
});
82+
83+
it('should not execute the scrollable events in the Angular zone', () => {
84+
const spy = jasmine.createSpy('zone unstable callback');
85+
const subscription = fixture.ngZone.onUnstable.subscribe(spy);
86+
87+
dispatchFakeEvent(fixture.componentInstance.scrollingElement.nativeElement, 'scroll');
88+
89+
expect(spy).not.toHaveBeenCalled();
90+
subscription.unsubscribe();
91+
});
7092
});
7193

7294
describe('Nested scrollables', () => {

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

+13-8
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import {Injectable, ElementRef, Optional, SkipSelf} from '@angular/core';
1+
import {Injectable, ElementRef, Optional, SkipSelf, NgZone} from '@angular/core';
22
import {Scrollable} from './scrollable';
33
import {Subject} from 'rxjs/Subject';
44
import {Observable} from 'rxjs/Observable';
@@ -17,6 +17,8 @@ export const DEFAULT_SCROLL_TIME = 20;
1717
*/
1818
@Injectable()
1919
export class ScrollDispatcher {
20+
constructor(private _ngZone: NgZone) { }
21+
2022
/** Subject for notifying that a registered scrollable reference element has been scrolled. */
2123
_scrolled: Subject<void> = new Subject<void>();
2224

@@ -69,10 +71,12 @@ export class ScrollDispatcher {
6971
this._scrolledCount++;
7072

7173
if (!this._globalSubscription) {
72-
this._globalSubscription = Observable.merge(
73-
Observable.fromEvent(window.document, 'scroll'),
74-
Observable.fromEvent(window, 'resize')
75-
).subscribe(() => this._notify());
74+
this._globalSubscription = this._ngZone.runOutsideAngular(() => {
75+
return Observable.merge(
76+
Observable.fromEvent(window.document, 'scroll'),
77+
Observable.fromEvent(window, 'resize')
78+
).subscribe(() => this._notify());
79+
});
7680
}
7781

7882
// Note that we need to do the subscribing from here, in order to be able to remove
@@ -118,13 +122,14 @@ export class ScrollDispatcher {
118122
}
119123
}
120124

121-
export function SCROLL_DISPATCHER_PROVIDER_FACTORY(parentDispatcher: ScrollDispatcher) {
122-
return parentDispatcher || new ScrollDispatcher();
125+
export function SCROLL_DISPATCHER_PROVIDER_FACTORY(parentDispatcher: ScrollDispatcher,
126+
ngZone: NgZone) {
127+
return parentDispatcher || new ScrollDispatcher(ngZone);
123128
}
124129

125130
export const SCROLL_DISPATCHER_PROVIDER = {
126131
// If there is already a ScrollDispatcher available, use that. Otherwise, provide a new one.
127132
provide: ScrollDispatcher,
128-
deps: [[new Optional(), new SkipSelf(), ScrollDispatcher]],
133+
deps: [[new Optional(), new SkipSelf(), ScrollDispatcher], NgZone],
129134
useFactory: SCROLL_DISPATCHER_PROVIDER_FACTORY
130135
};

src/lib/core/overlay/scroll/scrollable.ts

+20-3
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
1-
import {Directive, ElementRef, OnInit, OnDestroy} from '@angular/core';
1+
import {Directive, ElementRef, OnInit, OnDestroy, NgZone, Renderer} from '@angular/core';
22
import {Observable} from 'rxjs/Observable';
3+
import {Subject} from 'rxjs/Subject';
34
import {ScrollDispatcher} from './scroll-dispatcher';
45
import 'rxjs/add/observable/fromEvent';
56

@@ -13,22 +14,38 @@ import 'rxjs/add/observable/fromEvent';
1314
selector: '[cdk-scrollable]'
1415
})
1516
export class Scrollable implements OnInit, OnDestroy {
17+
private _elementScrolled: Subject<Event> = new Subject();
18+
private _scrollListener: Function;
19+
1620
constructor(private _elementRef: ElementRef,
17-
private _scroll: ScrollDispatcher) {}
21+
private _scroll: ScrollDispatcher,
22+
private _ngZone: NgZone,
23+
private _renderer: Renderer) {}
1824

1925
ngOnInit() {
26+
this._scrollListener = this._ngZone.runOutsideAngular(() => {
27+
return this._renderer.listen(this.getElementRef().nativeElement, 'scroll', (event: Event) => {
28+
this._elementScrolled.next(event);
29+
});
30+
});
31+
2032
this._scroll.register(this);
2133
}
2234

2335
ngOnDestroy() {
2436
this._scroll.deregister(this);
37+
38+
if (this._scrollListener) {
39+
this._scrollListener();
40+
this._scrollListener = null;
41+
}
2542
}
2643

2744
/**
2845
* Returns observable that emits when a scroll event is fired on the host element.
2946
*/
3047
elementScrolled(): Observable<any> {
31-
return Observable.fromEvent(this._elementRef.nativeElement, 'scroll');
48+
return this._elementScrolled.asObservable();
3249
}
3350

3451
getElementRef(): ElementRef {

src/lib/core/testing/dispatch-events.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import {
55
} from './event-objects';
66

77
/** Shorthand to dispatch a fake event on a specified node. */
8-
export function dispatchFakeEvent(node: Node, type: string) {
8+
export function dispatchFakeEvent(node: Node | Window, type: string) {
99
node.dispatchEvent(createFakeEvent(type));
1010
}
1111

0 commit comments

Comments
 (0)