From 22bacb17b3727e3908ddf709042e966c0ed9d766 Mon Sep 17 00:00:00 2001 From: Miles Malerba Date: Thu, 22 Feb 2018 13:45:41 -0800 Subject: [PATCH 1/2] virtual-scroll: only move views that need to be moved --- .../scrolling/virtual-for-of.ts | 124 +++++++----------- src/demo-app/demo-app-module.ts | 8 +- 2 files changed, 55 insertions(+), 77 deletions(-) diff --git a/src/cdk-experimental/scrolling/virtual-for-of.ts b/src/cdk-experimental/scrolling/virtual-for-of.ts index 848fc024667a..79b9f5fd25fb 100644 --- a/src/cdk-experimental/scrolling/virtual-for-of.ts +++ b/src/cdk-experimental/scrolling/virtual-for-of.ts @@ -45,12 +45,6 @@ export type CdkVirtualForOfContext = { }; -type RecordViewTuple = { - record: IterableChangeRecord | null, - view?: EmbeddedViewRef> -}; - - /** * A directive similar to `ngForOf` to be used for rendering data inside a virtual scrolling * container. @@ -101,6 +95,8 @@ export class CdkVirtualForOf implements CollectionViewer, DoCheck, OnDestroy } } + @Input() cdkVirtualForTemplateCacheSize: number = 20; + /** Emits whenever the data in the current DataSource changes. */ dataStream: Observable = this._dataSourceChanges .pipe( @@ -256,82 +252,64 @@ export class CdkVirtualForOf implements CollectionViewer, DoCheck, OnDestroy /** Apply changes to the DOM. */ private _applyChanges(changes: IterableChanges) { - // TODO(mmalerba): Currently we remove every view and then re-insert it in the correct place. - // It would be better to generate the minimal set of remove & inserts to get to the new list - // instead. - - // Detach all of the views and add them into an array to preserve their original order. - const previousViews: (EmbeddedViewRef> | null)[] = []; - let i = this._viewContainerRef.length; - while (i--) { - previousViews.unshift( - this._viewContainerRef.detach()! as EmbeddedViewRef>); - } - - // Mark the removed indices so we can recycle their views. - changes.forEachRemovedItem(record => { - this._templateCache.push(previousViews[record.previousIndex!]!); - previousViews[record.previousIndex!] = null; - }); - - // Queue up the newly added items to be inserted, recycling views from the cache if possible. - const insertTuples: RecordViewTuple[] = []; - changes.forEachAddedItem(record => { - insertTuples[record.currentIndex!] = {record, view: this._templateCache.pop()}; - }); - - // Queue up moved items to be re-inserted. - changes.forEachMovedItem(record => { - insertTuples[record.currentIndex!] = {record, view: previousViews[record.previousIndex!]!}; - previousViews[record.previousIndex!] = null; + // Rearrange the views to put them in the right location. + changes.forEachOperation( + (record: IterableChangeRecord, adjustedPreviousIndex: number, currentIndex: number) => { + if (record.previousIndex == null) { // Item added. + const view = this._getViewForNewItem(); + this._viewContainerRef.insert(view, currentIndex); + view.context.$implicit = record.item; + } else if (currentIndex == null) { // Item removed. + this._cacheView(this._viewContainerRef.detach(adjustedPreviousIndex) as + EmbeddedViewRef>); + } else { // Item moved. + const view = this._viewContainerRef.get(adjustedPreviousIndex) as + EmbeddedViewRef>; + this._viewContainerRef.move(view, currentIndex); + view.context.$implicit = record.item; + } + }); + + // Update $implicit for any items that had an identity change. + changes.forEachIdentityChange((record: any) => { + const view = this._viewContainerRef.get(record.currentIndex) as + EmbeddedViewRef>; + view.context.$implicit = record.item; }); - // We have nulled-out all of the views that were removed or moved from previousViews. What is - // left is the unchanged items that we queue up to be re-inserted. - i = previousViews.length; + // Update the context variables on all items. + let i = this._viewContainerRef.length, count = this._data.length; while (i--) { - if (previousViews[i]) { - insertTuples[i] = {record: null, view: previousViews[i]!}; - } + const view = this._viewContainerRef.get(i) as EmbeddedViewRef>; + view.context.index = this._renderedRange.start + i; + view.context.count = count; + this._updateComputedContextProperties(view.context); } - - // We now have a full list of everything to be inserted, so go ahead and insert them. - this._insertViews(insertTuples); } - /** Insert the RecordViewTuples into the container element. */ - private _insertViews(insertTuples: RecordViewTuple[]) { - const count = this._data.length; - let i = insertTuples.length; - const lastIndex = i - 1; - while (i--) { - const index = lastIndex - i; - let {view, record} = insertTuples[index]; - if (view) { - this._viewContainerRef.insert(view); - } else { - view = this._viewContainerRef.createEmbeddedView(this._template, { - $implicit: null!, - cdkVirtualForOf: this._cdkVirtualForOf, - index: -1, - count: -1, - first: false, - last: false, - odd: false, - even: false - }); - } - - if (record) { - view.context.$implicit = record.item as T; - } - view.context.index = this._renderedRange.start + index; - view.context.count = count; - this._updateComputedContextProperties(view.context); - view.detectChanges(); + /** Cache the given detached view. */ + private _cacheView(view: EmbeddedViewRef>) { + if (this._templateCache.length < this.cdkVirtualForTemplateCacheSize) { + this._templateCache.push(view); + } else { + view.destroy(); } } + /** Get a view for a new item, either from the cache or by creating a new one. */ + private _getViewForNewItem(): EmbeddedViewRef> { + return this._templateCache.pop() || this._viewContainerRef.createEmbeddedView(this._template, { + $implicit: null!, + cdkVirtualForOf: this._cdkVirtualForOf, + index: -1, + count: -1, + first: false, + last: false, + odd: false, + even: false + }); + } + /** Update the computed properties on the `CdkVirtualForOfContext`. */ private _updateComputedContextProperties(context: CdkVirtualForOfContext) { context.first = context.index === 0; diff --git a/src/demo-app/demo-app-module.ts b/src/demo-app/demo-app-module.ts index 5e42bbf01d24..f954aac852a5 100644 --- a/src/demo-app/demo-app-module.ts +++ b/src/demo-app/demo-app-module.ts @@ -6,15 +6,15 @@ * found in the LICENSE file at https://angular.io/license */ +import {HttpClientModule} from '@angular/common/http'; import {ApplicationRef, NgModule} from '@angular/core'; import {BrowserModule} from '@angular/platform-browser'; -import {HttpClientModule} from '@angular/common/http'; -import {RouterModule} from '@angular/router'; import {BrowserAnimationsModule} from '@angular/platform-browser/animations'; -import {ALL_ROUTES} from './demo-app/routes'; +import {RouterModule} from '@angular/router'; +import {AccessibilityDemoModule} from './a11y/a11y-module'; import {EntryApp} from './demo-app/demo-app'; import {DemoModule} from './demo-app/demo-module'; -import {AccessibilityDemoModule} from './a11y/a11y-module'; +import {ALL_ROUTES} from './demo-app/routes'; @NgModule({ From 2dbafee62391dac21a7e7a14c87da82c68cfd535 Mon Sep 17 00:00:00 2001 From: Miles Malerba Date: Mon, 26 Feb 2018 09:56:49 -0800 Subject: [PATCH 2/2] address comments --- src/cdk-experimental/scrolling/virtual-for-of.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/cdk-experimental/scrolling/virtual-for-of.ts b/src/cdk-experimental/scrolling/virtual-for-of.ts index 79b9f5fd25fb..04caea9d65dd 100644 --- a/src/cdk-experimental/scrolling/virtual-for-of.ts +++ b/src/cdk-experimental/scrolling/virtual-for-of.ts @@ -271,14 +271,15 @@ export class CdkVirtualForOf implements CollectionViewer, DoCheck, OnDestroy }); // Update $implicit for any items that had an identity change. - changes.forEachIdentityChange((record: any) => { - const view = this._viewContainerRef.get(record.currentIndex) as + changes.forEachIdentityChange((record: IterableChangeRecord) => { + const view = this._viewContainerRef.get(record.currentIndex!) as EmbeddedViewRef>; view.context.$implicit = record.item; }); // Update the context variables on all items. - let i = this._viewContainerRef.length, count = this._data.length; + const count = this._data.length; + let i = this._viewContainerRef.length; while (i--) { const view = this._viewContainerRef.get(i) as EmbeddedViewRef>; view.context.index = this._renderedRange.start + i;