Skip to content

Commit 8438ba8

Browse files
committed
virtual-scroll: use detectChanges rather than markForCheck in
`CdkVirtualScrollViewport`
1 parent a1aa9e7 commit 8438ba8

File tree

1 file changed

+77
-74
lines changed

1 file changed

+77
-74
lines changed

src/cdk-experimental/scrolling/virtual-scroll-viewport.ts

Lines changed: 77 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ function rangesEqual(r1: ListRange, r2: ListRange): boolean {
4848
encapsulation: ViewEncapsulation.None,
4949
changeDetection: ChangeDetectionStrategy.OnPush,
5050
})
51-
export class CdkVirtualScrollViewport implements DoCheck, OnInit, OnDestroy {
51+
export class CdkVirtualScrollViewport implements OnInit, OnDestroy {
5252
/** Emits when the viewport is detached from a CdkVirtualForOf. */
5353
private _detachedSubject = new Subject<void>();
5454

@@ -102,37 +102,29 @@ export class CdkVirtualScrollViewport implements DoCheck, OnInit, OnDestroy {
102102
/** Observable that emits when the viewport is destroyed. */
103103
private _destroyed = new Subject<void>();
104104

105+
/** Whether there is a pending change detection cycle. */
106+
private _checkPending = false;
107+
108+
/** A list of functions to run after the next change detection cycle. */
109+
private _runAfterCheck: Function[] = [];
110+
105111
constructor(public elementRef: ElementRef, private _changeDetectorRef: ChangeDetectorRef,
106112
private _ngZone: NgZone, private _sanitizer: DomSanitizer,
107113
@Inject(VIRTUAL_SCROLL_STRATEGY) private _scrollStrategy: VirtualScrollStrategy) {}
108114

109115
ngOnInit() {
110116
// It's still too early to measure the viewport at this point. Deferring with a promise allows
111117
// the Viewport to be rendered with the correct size before we measure.
112-
Promise.resolve().then(() => {
118+
this._ngZone.runOutsideAngular(() => Promise.resolve().then(() => {
113119
this._measureViewportSize();
114120
this._scrollStrategy.attach(this);
115121

116-
this._ngZone.runOutsideAngular(() => {
117-
fromEvent(this.elementRef.nativeElement, 'scroll')
118-
// Sample the scroll stream at every animation frame. This way if there are multiple
119-
// scroll events in the same frame we only need to recheck our layout once.
120-
.pipe(sampleTime(0, animationFrameScheduler), takeUntil(this._destroyed))
121-
.subscribe(() => this._scrollStrategy.onContentScrolled());
122-
});
123-
});
124-
}
125-
126-
ngDoCheck() {
127-
// In order to batch setting the scroll offset together with other DOM writes, we wait until a
128-
// change detection cycle to actually apply it.
129-
if (this._pendingScrollOffset != null) {
130-
if (this.orientation === 'horizontal') {
131-
this.elementRef.nativeElement.scrollLeft = this._pendingScrollOffset;
132-
} else {
133-
this.elementRef.nativeElement.scrollTop = this._pendingScrollOffset;
134-
}
135-
}
122+
fromEvent(this.elementRef.nativeElement, 'scroll')
123+
// Sample the scroll stream at every animation frame. This way if there are multiple
124+
// scroll events in the same frame we only need to recheck our layout once.
125+
.pipe(sampleTime(0, animationFrameScheduler), takeUntil(this._destroyed))
126+
.subscribe(() => this._scrollStrategy.onContentScrolled());
127+
}));
136128
}
137129

138130
ngOnDestroy() {
@@ -151,16 +143,18 @@ export class CdkVirtualScrollViewport implements DoCheck, OnInit, OnDestroy {
151143
if (this._forOf) {
152144
throw Error('CdkVirtualScrollViewport is already attached.');
153145
}
154-
this._forOf = forOf;
155146

156147
// Subscribe to the data stream of the CdkVirtualForOf to keep track of when the data length
157148
// changes.
158-
this._forOf.dataStream.pipe(takeUntil(this._detachedSubject)).subscribe(data => {
159-
const len = data.length;
160-
if (len !== this._dataLength) {
161-
this._dataLength = len;
162-
this._scrollStrategy.onDataLengthChanged();
163-
}
149+
this._ngZone.runOutsideAngular(() => {
150+
this._forOf = forOf;
151+
this._forOf.dataStream.pipe(takeUntil(this._detachedSubject)).subscribe(data => {
152+
const len = data.length;
153+
if (len !== this._dataLength) {
154+
this._dataLength = len;
155+
this._scrollStrategy.onDataLengthChanged();
156+
}
157+
});
164158
});
165159
}
166160

@@ -190,40 +184,22 @@ export class CdkVirtualScrollViewport implements DoCheck, OnInit, OnDestroy {
190184
return this._renderedRange;
191185
}
192186

193-
// TODO(mmalebra): Consider calling `detectChanges()` directly rather than the methods below.
194-
195187
/**
196188
* Sets the total size of all content (in pixels), including content that is not currently
197189
* rendered.
198190
*/
199191
setTotalContentSize(size: number) {
200192
if (this._totalContentSize !== size) {
201-
// Re-enter the Angular zone so we can mark for change detection.
202-
this._ngZone.run(() => {
203-
this._totalContentSize = size;
204-
this._changeDetectorRef.markForCheck();
205-
});
193+
this._totalContentSize = size;
194+
this._markForCheck();
206195
}
207196
}
208197

209198
/** Sets the currently rendered range of indices. */
210199
setRenderedRange(range: ListRange) {
211200
if (!rangesEqual(this._renderedRange, range)) {
212-
// Re-enter the Angular zone so we can mark for change detection.
213-
this._ngZone.run(() => {
214-
this._renderedRangeSubject.next(this._renderedRange = range);
215-
this._changeDetectorRef.markForCheck();
216-
});
217-
// Queue this up in a `Promise.resolve()` so that if the user makes a series of calls
218-
// like:
219-
//
220-
// viewport.setRenderedRange(...);
221-
// viewport.setTotalContentSize(...);
222-
// viewport.setRenderedContentOffset(...);
223-
//
224-
// The call to `onContentRendered` will happen after all of the updates have been applied.
225-
this._ngZone.runOutsideAngular(() => this._ngZone.onStable.pipe(take(1)).subscribe(
226-
() => Promise.resolve().then(() => this._scrollStrategy.onContentRendered())));
201+
this._renderedRangeSubject.next(this._renderedRange = range);
202+
this._markForCheck(() => this._scrollStrategy.onContentRendered());
227203
}
228204
}
229205

@@ -250,25 +226,18 @@ export class CdkVirtualScrollViewport implements DoCheck, OnInit, OnDestroy {
250226
this._renderedContentOffsetNeedsRewrite = true;
251227
}
252228
if (this._rawRenderedContentTransform != transform) {
253-
// Re-enter the Angular zone so we can mark for change detection.
254-
this._ngZone.run(() => {
255-
// We know this value is safe because we parse `offset` with `Number()` before passing it
256-
// into the string.
257-
this._rawRenderedContentTransform = transform;
258-
this._renderedContentTransform = this._sanitizer.bypassSecurityTrustStyle(transform);
259-
this._changeDetectorRef.markForCheck();
260-
261-
// If the rendered content offset was specified as an offset to the end of the content,
262-
// rewrite it as an offset to the start of the content.
263-
this._ngZone.onStable.pipe(take(1)).subscribe(() => {
264-
if (this._renderedContentOffsetNeedsRewrite) {
265-
this._renderedContentOffset -= this.measureRenderedContentSize();
266-
this._renderedContentOffsetNeedsRewrite = false;
267-
this.setRenderedContentOffset(this._renderedContentOffset);
268-
} else {
269-
this._scrollStrategy.onRenderedOffsetChanged();
270-
}
271-
});
229+
// We know this value is safe because we parse `offset` with `Number()` before passing it
230+
// into the string.
231+
this._rawRenderedContentTransform = transform;
232+
this._renderedContentTransform = this._sanitizer.bypassSecurityTrustStyle(transform);
233+
this._markForCheck(() => {
234+
if (this._renderedContentOffsetNeedsRewrite) {
235+
this._renderedContentOffset -= this.measureRenderedContentSize();
236+
this._renderedContentOffsetNeedsRewrite = false;
237+
this.setRenderedContentOffset(this._renderedContentOffset);
238+
} else {
239+
this._scrollStrategy.onRenderedOffsetChanged();
240+
}
272241
});
273242
}
274243
}
@@ -277,10 +246,8 @@ export class CdkVirtualScrollViewport implements DoCheck, OnInit, OnDestroy {
277246
setScrollOffset(offset: number) {
278247
// Rather than setting the offset immediately, we batch it up to be applied along with other DOM
279248
// writes during the next change detection cycle.
280-
this._ngZone.run(() => {
281-
this._pendingScrollOffset = offset;
282-
this._changeDetectorRef.markForCheck();
283-
});
249+
this._pendingScrollOffset = offset;
250+
this._markForCheck();
284251
}
285252

286253
/** Gets the current scroll offset of the viewport (in pixels). */
@@ -319,4 +286,40 @@ export class CdkVirtualScrollViewport implements DoCheck, OnInit, OnDestroy {
319286
this._viewportSize = this.orientation === 'horizontal' ?
320287
viewportEl.clientWidth : viewportEl.clientHeight;
321288
}
289+
290+
/** Queue up change detection to run. */
291+
private _markForCheck(runAfter?: Function) {
292+
if (runAfter) {
293+
this._runAfterCheck.push(runAfter);
294+
}
295+
if (!this._checkPending) {
296+
this._checkPending = true;
297+
this._ngZone.runOutsideAngular(() => Promise.resolve().then(() => {
298+
if (this._ngZone.isStable) {
299+
this._doCheck();
300+
} else {
301+
this._ngZone.onStable.pipe(take(1)).subscribe(() => this._doCheck());
302+
}
303+
}));
304+
}
305+
}
306+
307+
/** Run change detection. */
308+
private _doCheck() {
309+
this._checkPending = false;
310+
this._ngZone.run(() => this._changeDetectorRef.detectChanges());
311+
// In order to batch setting the scroll offset together with other DOM writes, we wait until a
312+
// change detection cycle to actually apply it.
313+
if (this._pendingScrollOffset != null) {
314+
if (this.orientation === 'horizontal') {
315+
this.elementRef.nativeElement.scrollLeft = this._pendingScrollOffset;
316+
} else {
317+
this.elementRef.nativeElement.scrollTop = this._pendingScrollOffset;
318+
}
319+
}
320+
for(let fn of this._runAfterCheck) {
321+
fn();
322+
}
323+
this._runAfterCheck = [];
324+
}
322325
}

0 commit comments

Comments
 (0)