-
Notifications
You must be signed in to change notification settings - Fork 6.8k
virtual-scroll: use detectChanges
rather than markForCheck
in CdkVirtualScrollViewport
#12158
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
Changes from 1 commit
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -48,7 +48,7 @@ function rangesEqual(r1: ListRange, r2: ListRange): boolean { | |
encapsulation: ViewEncapsulation.None, | ||
changeDetection: ChangeDetectionStrategy.OnPush, | ||
}) | ||
export class CdkVirtualScrollViewport implements DoCheck, OnInit, OnDestroy { | ||
export class CdkVirtualScrollViewport implements OnInit, OnDestroy { | ||
/** Emits when the viewport is detached from a CdkVirtualForOf. */ | ||
private _detachedSubject = new Subject<void>(); | ||
|
||
|
@@ -102,37 +102,29 @@ export class CdkVirtualScrollViewport implements DoCheck, OnInit, OnDestroy { | |
/** Observable that emits when the viewport is destroyed. */ | ||
private _destroyed = new Subject<void>(); | ||
|
||
/** Whether there is a pending change detection cycle. */ | ||
private _checkPending = false; | ||
|
||
/** A list of functions to run after the next change detection cycle. */ | ||
private _runAfterCheck: Function[] = []; | ||
|
||
constructor(public elementRef: ElementRef, private _changeDetectorRef: ChangeDetectorRef, | ||
private _ngZone: NgZone, private _sanitizer: DomSanitizer, | ||
@Inject(VIRTUAL_SCROLL_STRATEGY) private _scrollStrategy: VirtualScrollStrategy) {} | ||
|
||
ngOnInit() { | ||
// It's still too early to measure the viewport at this point. Deferring with a promise allows | ||
// the Viewport to be rendered with the correct size before we measure. | ||
Promise.resolve().then(() => { | ||
this._ngZone.runOutsideAngular(() => Promise.resolve().then(() => { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Expand comment to explain why you run outside the zone? |
||
this._measureViewportSize(); | ||
this._scrollStrategy.attach(this); | ||
|
||
this._ngZone.runOutsideAngular(() => { | ||
fromEvent(this.elementRef.nativeElement, 'scroll') | ||
// Sample the scroll stream at every animation frame. This way if there are multiple | ||
// scroll events in the same frame we only need to recheck our layout once. | ||
.pipe(sampleTime(0, animationFrameScheduler), takeUntil(this._destroyed)) | ||
.subscribe(() => this._scrollStrategy.onContentScrolled()); | ||
}); | ||
}); | ||
} | ||
|
||
ngDoCheck() { | ||
// In order to batch setting the scroll offset together with other DOM writes, we wait until a | ||
// change detection cycle to actually apply it. | ||
if (this._pendingScrollOffset != null) { | ||
if (this.orientation === 'horizontal') { | ||
this.elementRef.nativeElement.scrollLeft = this._pendingScrollOffset; | ||
} else { | ||
this.elementRef.nativeElement.scrollTop = this._pendingScrollOffset; | ||
} | ||
} | ||
fromEvent(this.elementRef.nativeElement, 'scroll') | ||
// Sample the scroll stream at every animation frame. This way if there are multiple | ||
// scroll events in the same frame we only need to recheck our layout once. | ||
.pipe(sampleTime(0, animationFrameScheduler), takeUntil(this._destroyed)) | ||
.subscribe(() => this._scrollStrategy.onContentScrolled()); | ||
})); | ||
} | ||
|
||
ngOnDestroy() { | ||
|
@@ -151,16 +143,18 @@ export class CdkVirtualScrollViewport implements DoCheck, OnInit, OnDestroy { | |
if (this._forOf) { | ||
throw Error('CdkVirtualScrollViewport is already attached.'); | ||
} | ||
this._forOf = forOf; | ||
|
||
// Subscribe to the data stream of the CdkVirtualForOf to keep track of when the data length | ||
// changes. | ||
this._forOf.dataStream.pipe(takeUntil(this._detachedSubject)).subscribe(data => { | ||
const len = data.length; | ||
if (len !== this._dataLength) { | ||
this._dataLength = len; | ||
this._scrollStrategy.onDataLengthChanged(); | ||
} | ||
this._ngZone.runOutsideAngular(() => { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Add comment that explains why this is going out of the zone? |
||
this._forOf = forOf; | ||
this._forOf.dataStream.pipe(takeUntil(this._detachedSubject)).subscribe(data => { | ||
const len = data.length; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should this be called something like |
||
if (len !== this._dataLength) { | ||
this._dataLength = len; | ||
this._scrollStrategy.onDataLengthChanged(); | ||
} | ||
}); | ||
}); | ||
} | ||
|
||
|
@@ -190,40 +184,22 @@ export class CdkVirtualScrollViewport implements DoCheck, OnInit, OnDestroy { | |
return this._renderedRange; | ||
} | ||
|
||
// TODO(mmalebra): Consider calling `detectChanges()` directly rather than the methods below. | ||
|
||
/** | ||
* Sets the total size of all content (in pixels), including content that is not currently | ||
* rendered. | ||
*/ | ||
setTotalContentSize(size: number) { | ||
if (this._totalContentSize !== size) { | ||
// Re-enter the Angular zone so we can mark for change detection. | ||
this._ngZone.run(() => { | ||
this._totalContentSize = size; | ||
this._changeDetectorRef.markForCheck(); | ||
}); | ||
this._totalContentSize = size; | ||
this._markForCheck(); | ||
} | ||
} | ||
|
||
/** Sets the currently rendered range of indices. */ | ||
setRenderedRange(range: ListRange) { | ||
if (!rangesEqual(this._renderedRange, range)) { | ||
// Re-enter the Angular zone so we can mark for change detection. | ||
this._ngZone.run(() => { | ||
this._renderedRangeSubject.next(this._renderedRange = range); | ||
this._changeDetectorRef.markForCheck(); | ||
}); | ||
// Queue this up in a `Promise.resolve()` so that if the user makes a series of calls | ||
// like: | ||
// | ||
// viewport.setRenderedRange(...); | ||
// viewport.setTotalContentSize(...); | ||
// viewport.setRenderedContentOffset(...); | ||
// | ||
// The call to `onContentRendered` will happen after all of the updates have been applied. | ||
this._ngZone.runOutsideAngular(() => this._ngZone.onStable.pipe(take(1)).subscribe( | ||
() => Promise.resolve().then(() => this._scrollStrategy.onContentRendered()))); | ||
this._renderedRangeSubject.next(this._renderedRange = range); | ||
this._markForCheck(() => this._scrollStrategy.onContentRendered()); | ||
} | ||
} | ||
|
||
|
@@ -250,25 +226,18 @@ export class CdkVirtualScrollViewport implements DoCheck, OnInit, OnDestroy { | |
this._renderedContentOffsetNeedsRewrite = true; | ||
} | ||
if (this._rawRenderedContentTransform != transform) { | ||
// Re-enter the Angular zone so we can mark for change detection. | ||
this._ngZone.run(() => { | ||
// We know this value is safe because we parse `offset` with `Number()` before passing it | ||
// into the string. | ||
this._rawRenderedContentTransform = transform; | ||
this._renderedContentTransform = this._sanitizer.bypassSecurityTrustStyle(transform); | ||
this._changeDetectorRef.markForCheck(); | ||
|
||
// If the rendered content offset was specified as an offset to the end of the content, | ||
// rewrite it as an offset to the start of the content. | ||
this._ngZone.onStable.pipe(take(1)).subscribe(() => { | ||
if (this._renderedContentOffsetNeedsRewrite) { | ||
this._renderedContentOffset -= this.measureRenderedContentSize(); | ||
this._renderedContentOffsetNeedsRewrite = false; | ||
this.setRenderedContentOffset(this._renderedContentOffset); | ||
} else { | ||
this._scrollStrategy.onRenderedOffsetChanged(); | ||
} | ||
}); | ||
// We know this value is safe because we parse `offset` with `Number()` before passing it | ||
// into the string. | ||
this._rawRenderedContentTransform = transform; | ||
this._renderedContentTransform = this._sanitizer.bypassSecurityTrustStyle(transform); | ||
this._markForCheck(() => { | ||
if (this._renderedContentOffsetNeedsRewrite) { | ||
this._renderedContentOffset -= this.measureRenderedContentSize(); | ||
this._renderedContentOffsetNeedsRewrite = false; | ||
this.setRenderedContentOffset(this._renderedContentOffset); | ||
} else { | ||
this._scrollStrategy.onRenderedOffsetChanged(); | ||
} | ||
}); | ||
} | ||
} | ||
|
@@ -277,10 +246,8 @@ export class CdkVirtualScrollViewport implements DoCheck, OnInit, OnDestroy { | |
setScrollOffset(offset: number) { | ||
// Rather than setting the offset immediately, we batch it up to be applied along with other DOM | ||
// writes during the next change detection cycle. | ||
this._ngZone.run(() => { | ||
this._pendingScrollOffset = offset; | ||
this._changeDetectorRef.markForCheck(); | ||
}); | ||
this._pendingScrollOffset = offset; | ||
this._markForCheck(); | ||
} | ||
|
||
/** Gets the current scroll offset of the viewport (in pixels). */ | ||
|
@@ -319,4 +286,40 @@ export class CdkVirtualScrollViewport implements DoCheck, OnInit, OnDestroy { | |
this._viewportSize = this.orientation === 'horizontal' ? | ||
viewportEl.clientWidth : viewportEl.clientHeight; | ||
} | ||
|
||
/** Queue up change detection to run. */ | ||
private _markForCheck(runAfter?: Function) { | ||
if (runAfter) { | ||
this._runAfterCheck.push(runAfter); | ||
} | ||
if (!this._checkPending) { | ||
this._checkPending = true; | ||
this._ngZone.runOutsideAngular(() => Promise.resolve().then(() => { | ||
if (this._ngZone.isStable) { | ||
this._doCheck(); | ||
} else { | ||
this._ngZone.onStable.pipe(take(1)).subscribe(() => this._doCheck()); | ||
} | ||
})); | ||
} | ||
} | ||
|
||
/** Run change detection. */ | ||
private _doCheck() { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Call this method something like |
||
this._checkPending = false; | ||
this._ngZone.run(() => this._changeDetectorRef.detectChanges()); | ||
// In order to batch setting the scroll offset together with other DOM writes, we wait until a | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't quite follow what this comment means about waiting, since There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes, but in the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should the comment live where the wait is scheduled? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I split the comment up to explain the batching part where it actually happens |
||
// change detection cycle to actually apply it. | ||
if (this._pendingScrollOffset != null) { | ||
if (this.orientation === 'horizontal') { | ||
this.elementRef.nativeElement.scrollLeft = this._pendingScrollOffset; | ||
} else { | ||
this.elementRef.nativeElement.scrollTop = this._pendingScrollOffset; | ||
} | ||
} | ||
for(let fn of this._runAfterCheck) { | ||
fn(); | ||
} | ||
this._runAfterCheck = []; | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
_isPendingChangeDetection
or_needsChangeDetection
?