mirror of
https://github.com/ionic-team/ionic-framework.git
synced 2025-08-23 22:17:40 +08:00
feat(virtual-scroll): items update fast path
This commit is contained in:
@ -381,6 +381,12 @@ which is an expensive operation and should be avoided if possible.
|
||||
|
||||
## Methods
|
||||
|
||||
#### markDirty()
|
||||
|
||||
|
||||
#### markDirtyTail()
|
||||
|
||||
|
||||
#### positionForItem()
|
||||
|
||||
|
||||
|
@ -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>
|
||||
|
@ -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 };
|
||||
|
@ -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);
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
Reference in New Issue
Block a user