feat(virtual-scroll): items update fast path

This commit is contained in:
Manu Mtz.-Almeida
2018-02-09 21:53:30 +01:00
parent 77e16bbc3f
commit 3462ed8985
5 changed files with 111 additions and 24 deletions

View File

@ -381,6 +381,12 @@ which is an expensive operation and should be avoided if possible.
## Methods
#### markDirty()
#### markDirtyTail()
#### positionForItem()

View File

@ -33,17 +33,20 @@
</ion-app>
<script>
const virtual = document.getElementById('virtual');
const items = Array.from({length: 100}, (x, i) => i);
function addItems() {
const virtual = document.getElementById('virtual');
virtual.addItems()
const append = Array.from({length: 10}, (x, i) => "append" + i);
items.push(...append);
virtual.markDirtyTail(append.length)
}
const virtual = document.getElementById('virtual');
virtual.itemHeight = () => 45;
virtual.headerFn = (item, index) => {
if (index % 20 === 0) {
return 'Header ' + index;
}
// if (index % 20 === 0) {
// return 'Header ' + index;
// }
return null;
}
@ -75,7 +78,7 @@
if (cell.type === 0) return renderItem(el, cell.value);
return renderHeader(el, cell.value);
};
virtual.items = Array.from({length: 1000}, (x, i) => i);
virtual.items = items;
</script>
</body>
</html>

View File

@ -60,7 +60,7 @@ describe('getRange', () => {
expect(bounds).toEqual({
offset: 0,
length: 4,
length: 5,
});
});
@ -148,7 +148,7 @@ describe('resizeBuffer', () => {
describe('calcCells', () => {
it('should calculate cells without headers and itemHeight', () => {
const items = ['0', 2, 'hola', {data: 'hello'}];
const cells = calcCells(items, null, null, null, 10, 20, 30);
const cells = calcCells(items, null, null, null, 10, 20, 30, 0, 0, items.length);
expect(cells).toEqual([
{
type: CellType.Item,
@ -197,7 +197,7 @@ describe('calcCells', () => {
called++;
return index * 20 + 20;
};
const cells = calcCells(items, itemHeight, null, null, 10, 20, 30);
const cells = calcCells(items, itemHeight, null, null, 10, 20, 30, 0, 0, items.length);
expect(called).toEqual(3);
expect(cells).toEqual([
@ -253,7 +253,7 @@ describe('calcCells', () => {
called++;
return index * 20 + 20;
};
const cells = calcCells(items, itemHeight, headerFn, footerFn, 10, 20, 30);
const cells = calcCells(items, itemHeight, headerFn, footerFn, 10, 20, 30, 0, 0, items.length);
expect(cells).toHaveLength(5);
expect(called).toEqual(3);
expect(headerCalled).toEqual(3);
@ -317,7 +317,7 @@ describe('calcHeightIndex', () => {
const footerFn: HeaderFn = (_, index) => {
return (index === 2) ? 'my footer' : null;
};
const cells = calcCells(items, null, headerFn, footerFn, 10, 20, 50);
const cells = calcCells(items, null, headerFn, footerFn, 10, 20, 50, 0, 0, items.length);
const buf = resizeBuffer(null, cells.length);
const totalHeight = calcHeightIndex(buf, cells, 0);
expect(buf.length).toEqual(7);
@ -506,7 +506,7 @@ function mockVirtualScroll(
headerFn: HeaderFn = null,
footerFn: HeaderFn = null
) {
const cells = calcCells(items, itemHeight, headerFn, footerFn, 10, 10, 30);
const cells = calcCells(items, itemHeight, headerFn, footerFn, 10, 10, 30, 0, 0, items.length);
const heightIndex = resizeBuffer(null, cells.length);
calcHeightIndex(heightIndex, cells, 0);
return { items, heightIndex, cells };

View File

@ -148,7 +148,7 @@ export function doRender(
node.visible = visible;
}
// dynamic height inference
// dynamic height
if (cell.reads > 0) {
updateCellHeight(cell, child);
cell.reads--;
@ -182,7 +182,8 @@ export function getRange(heightIndex: Uint32Array, viewport: Viewport, buffer: n
break;
}
}
const end = Math.min(i + buffer, heightIndex.length - 1);
const end = Math.min(i + buffer, heightIndex.length);
const length = end - offset;
return { offset, length };
}
@ -197,6 +198,23 @@ export function getShouldUpdate(dirtyIndex: number, currentRange: Range, range:
}
export function findCellIndex(cells: Cell[], index: number): number {
if (index === 0) {
return 0;
}
return cells.findIndex(c => c.index === index);
}
export function inplaceUpdate(dst: Cell[], src: Cell[], offset: number) {
if (offset === 0 && src.length >= dst.length) {
return src;
}
for (let i = 0; i < src.length; i++) {
dst[i + offset] = src[i];
}
return dst;
}
export function calcCells(
items: any[],
@ -206,12 +224,15 @@ export function calcCells(
approxHeaderHeight: number,
approxFooterHeight: number,
approxItemHeight: number
approxItemHeight: number,
j: number,
offset: number,
len: number
): Cell[] {
const cells = [];
let j = 0;
for (let i = 0; i < items.length; i++) {
const end = len + offset;
for (let i = offset; i < end; i++) {
const item = items[i];
if (headerFn) {
const value = headerFn(item, i, items);

View File

@ -2,7 +2,7 @@ import { Component, Element, EventListenerEnable, Listen, Method, Prop, Watch }
import { DomController } from '../../index';
import { Cell, DomRenderFn, HeaderFn, ItemHeightFn, ItemRenderFn, NodeHeightFn, Range,
Viewport, VirtualNode, calcCells, calcHeightIndex, doRender, getRange,
getShouldUpdate, getViewport, positionForIndex, resizeBuffer, updateVDom } from './virtual-scroll-utils';
getShouldUpdate, getViewport, positionForIndex, resizeBuffer, updateVDom, findCellIndex, inplaceUpdate } from './virtual-scroll-utils';
@Component({
@ -24,6 +24,7 @@ export class VirtualScroll {
private indexDirty = 0;
private totalHeight = 0;
private heightChanged = false;
private lastItemLen = 0;
@Element() el: HTMLElement;
@ -144,6 +145,52 @@ export class VirtualScroll {
return positionForIndex(index, this.cells, this.heightIndex);
}
@Method()
markDirty(offset: number, len = -1) {
// TODO: kind of hacky how we do in-place updated of the cells
// array. this part needs a complete refactor
if (!this.items) {
return;
}
if (len === -1) {
len = this.items.length - offset;
}
const max = this.lastItemLen;
let j = 0;
if (offset > 0 && offset < max) {
j = findCellIndex(this.cells, offset);
} else if (offset === 0) {
j = 0;
} else if (offset === max) {
j = this.cells.length;
} else {
console.warn('bad values for markDirty');
return;
}
const cells = calcCells(
this.items,
this.itemHeight,
this.headerFn,
this.footerFn,
this.approxHeaderHeight,
this.approxFooterHeight,
this.approxItemHeight,
j, offset, len
);
console.debug('[virtual] cells recalculated', cells.length);
this.cells = inplaceUpdate(this.cells, cells, offset);
this.lastItemLen = this.items.length;
this.indexDirty = Math.max(offset - 1, 0);
this.scheduleUpdate();
}
@Method()
markDirtyTail() {
const offset = this.lastItemLen;
this.markDirty(offset, this.items.length - offset);
}
private updateVirtualScroll() {
// do nothing if there is a scheduled update
if (!this.isEnabled || !this.scrollEl) {
@ -229,14 +276,18 @@ export class VirtualScroll {
}
cell.visible = true;
if (cell.height !== height) {
console.debug(`[${cell.reads}] cell size ${cell.height} -> ${height}`);
console.debug(`[virtual] cell height changed ${cell.height}px -> ${height}px`);
cell.height = height;
clearTimeout(this.timerUpdate);
this.timerUpdate = setTimeout(() => this.updateVirtualScroll(), 100);
this.indexDirty = Math.min(this.indexDirty, index);
this.scheduleUpdate();
}
}
private scheduleUpdate() {
clearTimeout(this.timerUpdate);
this.timerUpdate = setTimeout(() => this.updateVirtualScroll(), 100);
}
private updateState() {
const shouldEnable = !!(
this.scrollEl &&
@ -252,10 +303,12 @@ export class VirtualScroll {
}
}
private calcCells() {
if (!this.items) {
return;
}
this.lastItemLen = this.items.length;
this.cells = calcCells(
this.items,
this.itemHeight,
@ -263,8 +316,10 @@ export class VirtualScroll {
this.footerFn,
this.approxHeaderHeight,
this.approxFooterHeight,
this.approxItemHeight
this.approxItemHeight,
0, 0, this.lastItemLen
);
console.debug('[virtual] cells recalculated', this.cells.length);
this.indexDirty = 0;
}
@ -279,9 +334,11 @@ export class VirtualScroll {
this.heightIndex = resizeBuffer(this.heightIndex, this.cells.length);
const totalHeight = calcHeightIndex(this.heightIndex, this.cells, index);
if (totalHeight !== this.totalHeight) {
console.debug(`[virtual] total height changed: ${this.totalHeight}px -> ${totalHeight}px`);
this.totalHeight = totalHeight;
this.heightChanged = true;
}
console.debug('[virtual] height index recalculated', this.heightIndex.length - index);
this.indexDirty = Infinity;
}