mirror of
https://github.com/urbit/shrub.git
synced 2024-12-19 16:51:42 +03:00
VirtualScroller: rework for less memory use, faster speeds
This commit is contained in:
parent
c1f055d46e
commit
aaea592cfc
@ -1,4 +1,4 @@
|
||||
import React, { Component, useCallback } from 'react';
|
||||
import React, { Component, useCallback, SyntheticEvent } from 'react';
|
||||
import _ from 'lodash';
|
||||
import normalizeWheel from 'normalize-wheel';
|
||||
import bigInt, { BigInteger } from 'big-integer';
|
||||
@ -76,7 +76,7 @@ interface VirtualScrollerProps<T> {
|
||||
}
|
||||
|
||||
interface VirtualScrollerState<T> {
|
||||
visibleItems: BigIntOrderedMap<T>;
|
||||
visibleItems: BigInteger[];
|
||||
scrollbar: number;
|
||||
loaded: {
|
||||
top: boolean;
|
||||
@ -91,10 +91,9 @@ const log = (level: LogLevel, message: string) => {
|
||||
if(logLevel.includes(level)) {
|
||||
console.log(`[${level}]: ${message}`);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
const ZONE_SIZE = IS_IOS ? 10 : 40;
|
||||
const ZONE_SIZE = IS_IOS ? 10 : 80;
|
||||
|
||||
|
||||
// nb: in this file, an index refers to a BigInteger and an offset refers to a
|
||||
@ -114,7 +113,7 @@ export default class VirtualScroller<T> extends Component<VirtualScrollerProps<T
|
||||
/**
|
||||
* A map of child refs, used to calculate scroll position
|
||||
*/
|
||||
private childRefs = new BigIntOrderedMap<HTMLElement>();
|
||||
private childRefs = new Map<string, HTMLElement>();
|
||||
/**
|
||||
* A set of child refs which have been unmounted
|
||||
*/
|
||||
@ -149,7 +148,7 @@ export default class VirtualScroller<T> extends Component<VirtualScrollerProps<T
|
||||
constructor(props: VirtualScrollerProps<T>) {
|
||||
super(props);
|
||||
this.state = {
|
||||
visibleItems: new BigIntOrderedMap(),
|
||||
visibleItems: [],
|
||||
scrollbar: 0,
|
||||
loaded: {
|
||||
top: false,
|
||||
@ -164,6 +163,7 @@ export default class VirtualScroller<T> extends Component<VirtualScrollerProps<T
|
||||
this.scrollKeyMap = this.scrollKeyMap.bind(this);
|
||||
this.setWindow = this.setWindow.bind(this);
|
||||
this.restore = this.restore.bind(this);
|
||||
this.startOffset = this.startOffset.bind(this);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
@ -181,7 +181,7 @@ export default class VirtualScroller<T> extends Component<VirtualScrollerProps<T
|
||||
}
|
||||
[...this.orphans].forEach(o => {
|
||||
const index = bigInt(o);
|
||||
this.childRefs.delete(index);
|
||||
this.childRefs.delete(index.toString());
|
||||
});
|
||||
this.orphans.clear();
|
||||
};
|
||||
@ -206,13 +206,10 @@ export default class VirtualScroller<T> extends Component<VirtualScrollerProps<T
|
||||
|
||||
componentDidUpdate(prevProps: VirtualScrollerProps<T>, _prevState: VirtualScrollerState<T>) {
|
||||
const { id, size, data, offset, pendingSize } = this.props;
|
||||
const { visibleItems } = this.state;
|
||||
|
||||
if(size !== prevProps.size || pendingSize !== prevProps.pendingSize) {
|
||||
if(this.scrollLocked && visibleItems?.peekLargest() && data?.peekLargest()) {
|
||||
if(!visibleItems.peekLargest()[0].eq(data.peekLargest()[0])) {
|
||||
this.updateVisible(0);
|
||||
}
|
||||
if(this.scrollLocked) {
|
||||
this.updateVisible(0);
|
||||
this.resetScroll();
|
||||
}
|
||||
}
|
||||
@ -228,11 +225,13 @@ export default class VirtualScroller<T> extends Component<VirtualScrollerProps<T
|
||||
}
|
||||
|
||||
startOffset() {
|
||||
const startIndex = this.state?.visibleItems?.peekLargest()?.[0];
|
||||
const { data } = this.props;
|
||||
const startIndex = this.state.visibleItems?.[0];
|
||||
if(!startIndex) {
|
||||
return 0;
|
||||
}
|
||||
const offset = [...this.props.data].findIndex(([i]) => i.eq(startIndex))
|
||||
const dataList = Array.from(data);
|
||||
const offset = dataList.findIndex(([i]) => i.eq(startIndex))
|
||||
if(offset === -1) {
|
||||
// TODO: revisit when we remove nodes for any other reason than
|
||||
// pending indices being removed
|
||||
@ -252,19 +251,17 @@ export default class VirtualScroller<T> extends Component<VirtualScrollerProps<T
|
||||
log('reflow', `from: ${this.startOffset()} to: ${newOffset}`);
|
||||
|
||||
const { data, onCalculateVisibleItems } = this.props;
|
||||
const visibleItems = new BigIntOrderedMap<any>(
|
||||
[...data].slice(newOffset, newOffset + this.pageSize)
|
||||
);
|
||||
const visibleItems = data.keys().slice(newOffset, newOffset + this.pageSize);
|
||||
|
||||
this.save();
|
||||
|
||||
this.setState({
|
||||
visibleItems,
|
||||
}, () => {
|
||||
requestAnimationFrame(() => {
|
||||
this.restore();
|
||||
});
|
||||
});
|
||||
requestAnimationFrame(() => {
|
||||
this.restore();
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
scrollKeyMap(): Map<string, number> {
|
||||
@ -296,7 +293,6 @@ export default class VirtualScroller<T> extends Component<VirtualScrollerProps<T
|
||||
setWindow(element) {
|
||||
if (!element)
|
||||
return;
|
||||
console.log('resetting window');
|
||||
this.save();
|
||||
|
||||
if (this.window) {
|
||||
@ -309,7 +305,7 @@ export default class VirtualScroller<T> extends Component<VirtualScrollerProps<T
|
||||
const { averageHeight } = this.props;
|
||||
|
||||
this.window = element;
|
||||
this.pageSize = Math.floor(element.offsetHeight / Math.floor(averageHeight / 5.5));
|
||||
this.pageSize = Math.floor(element.offsetHeight / Math.floor(averageHeight / 3));
|
||||
this.pageDelta = Math.floor(this.pageSize / 3);
|
||||
if (this.props.origin === 'bottom') {
|
||||
element.addEventListener('wheel', (event) => {
|
||||
@ -356,7 +352,7 @@ export default class VirtualScroller<T> extends Component<VirtualScrollerProps<T
|
||||
}
|
||||
};
|
||||
|
||||
onScroll(event: UIEvent) {
|
||||
onScroll(event: SyntheticEvent<HTMLElement, ScrollEvent>) {
|
||||
this.updateScroll();
|
||||
if(!this.window) {
|
||||
// bail if we're going to adjust scroll anyway
|
||||
@ -368,9 +364,11 @@ export default class VirtualScroller<T> extends Component<VirtualScrollerProps<T
|
||||
}
|
||||
const { onStartReached, onEndReached } = this.props;
|
||||
const windowHeight = this.window.offsetHeight;
|
||||
const { scrollTop, scrollHeight } = this.window;
|
||||
const { scrollTop, scrollHeight } = event.target as HTMLElement;
|
||||
|
||||
const startOffset = this.startOffset();
|
||||
|
||||
const scrollEnd = scrollTop + windowHeight;
|
||||
if (scrollTop < ZONE_SIZE) {
|
||||
log('scroll', `Entered start zone ${scrollTop}`);
|
||||
if (startOffset === 0) {
|
||||
@ -423,7 +421,7 @@ export default class VirtualScroller<T> extends Component<VirtualScrollerProps<T
|
||||
return;
|
||||
}
|
||||
|
||||
let ref = this.childRefs.get(this.savedIndex)
|
||||
let ref = this.childRefs.get(this.savedIndex.toString())
|
||||
if(!ref) {
|
||||
return;
|
||||
}
|
||||
@ -438,7 +436,7 @@ export default class VirtualScroller<T> extends Component<VirtualScrollerProps<T
|
||||
}
|
||||
|
||||
scrollToIndex = (index: BigInteger) => {
|
||||
let ref = this.childRefs.get(index);
|
||||
let ref = this.childRefs.get(index.toString());
|
||||
if(!ref) {
|
||||
const offset = [...this.props.data].findIndex(([idx]) => idx.eq(index));
|
||||
if(offset === -1) {
|
||||
@ -446,7 +444,7 @@ export default class VirtualScroller<T> extends Component<VirtualScrollerProps<T
|
||||
}
|
||||
this.updateVisible(Math.max(offset - this.pageDelta, 0));
|
||||
requestAnimationFrame(() => {
|
||||
ref = this.childRefs.get(index);
|
||||
ref = this.childRefs.get(index.toString());
|
||||
this.savedIndex = null;
|
||||
this.savedDistance = 0;
|
||||
this.saveDepth = 0;
|
||||
@ -467,17 +465,18 @@ export default class VirtualScroller<T> extends Component<VirtualScrollerProps<T
|
||||
return;
|
||||
}
|
||||
if(this.saveDepth !== 0) {
|
||||
console.log('bail', 'deep save');
|
||||
return;
|
||||
}
|
||||
|
||||
log('scroll', 'saving...');
|
||||
|
||||
this.saveDepth++;
|
||||
|
||||
let bottomIndex: BigInteger | null = null;
|
||||
const { scrollTop, scrollHeight } = this.window;
|
||||
const topSpacing = scrollHeight - scrollTop;
|
||||
[...Array.from(this.state.visibleItems)].reverse().forEach(([index, datum]) => {
|
||||
const el = this.childRefs.get(index);
|
||||
([...this.state.visibleItems]).reverse().forEach((index) => {
|
||||
const el = this.childRefs.get(index.toString());
|
||||
if(!el) {
|
||||
return;
|
||||
}
|
||||
@ -490,11 +489,12 @@ export default class VirtualScroller<T> extends Component<VirtualScrollerProps<T
|
||||
if(!bottomIndex) {
|
||||
// weird, shouldn't really happen
|
||||
this.saveDepth--;
|
||||
log('bail', 'no index found');
|
||||
return;
|
||||
}
|
||||
|
||||
this.savedIndex = bottomIndex;
|
||||
const ref = this.childRefs.get(bottomIndex)!;
|
||||
const ref = this.childRefs.get(bottomIndex.toString())!;
|
||||
const { offsetTop } = ref;
|
||||
this.savedDistance = topSpacing - offsetTop
|
||||
}
|
||||
@ -503,7 +503,7 @@ export default class VirtualScroller<T> extends Component<VirtualScrollerProps<T
|
||||
|
||||
setRef = (element: HTMLElement | null, index: BigInteger) => {
|
||||
if(element) {
|
||||
this.childRefs.set(index, element);
|
||||
this.childRefs.set(index.toString(), element);
|
||||
this.orphans.delete(index.toString());
|
||||
} else {
|
||||
this.orphans.add(index.toString());
|
||||
@ -525,11 +525,10 @@ export default class VirtualScroller<T> extends Component<VirtualScrollerProps<T
|
||||
|
||||
const isTop = origin === 'top';
|
||||
|
||||
const indexesToRender = isTop ? visibleItems.keys() : visibleItems.keys().reverse();
|
||||
|
||||
const transform = isTop ? 'scale3d(1, 1, 1)' : 'scale3d(1, -1, 1)';
|
||||
const children = isTop ? visibleItems : [...visibleItems].reverse();
|
||||
|
||||
const atStart = (this.props.data.peekLargest()?.[0] ?? bigInt.zero).eq(visibleItems.peekLargest()?.[0] || bigInt.zero);
|
||||
const atStart = (this.props.data.peekLargest()?.[0] ?? bigInt.zero).eq(visibleItems?.[0] || bigInt.zero);
|
||||
const atEnd = this.state.loaded.top;
|
||||
|
||||
return (
|
||||
@ -542,7 +541,7 @@ export default class VirtualScroller<T> extends Component<VirtualScrollerProps<T
|
||||
<LoadingSpinner />
|
||||
</Center>)}
|
||||
<VirtualContext.Provider value={this.shiftLayout}>
|
||||
{indexesToRender.map(index => (
|
||||
{children.map(index => (
|
||||
<VirtualChild
|
||||
key={index.toString()}
|
||||
setRef={this.setRef}
|
||||
@ -575,8 +574,10 @@ function VirtualChild(props: VirtualChildProps) {
|
||||
|
||||
const ref = useCallback((el: HTMLElement | null) => {
|
||||
setRef(el, props.index);
|
||||
}, [setRef, props.index])
|
||||
// VirtualChild should always be keyed on the index, so the index should be
|
||||
// valid for the entire lifecycle of the component, hence no dependencies
|
||||
}, []);
|
||||
|
||||
return (<Renderer ref={ref} {...rest} />);
|
||||
return <Renderer ref={ref} {...rest} />
|
||||
};
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user