Skip to content

Commit 0e7e5c2

Browse files
authored
feat: Support switch between virtual & raw list (#4)
* add skip to check if not need animation * fix test * add test case
1 parent e2d4306 commit 0e7e5c2

File tree

4 files changed

+259
-63
lines changed

4 files changed

+259
-63
lines changed

examples/animate.tsx

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ function genItem() {
1616
}
1717

1818
const originData: Item[] = [];
19-
for (let i = 0; i < 100000; i += 1) {
19+
for (let i = 0; i < 1000; i += 1) {
2020
originData.push(genItem());
2121
}
2222

@@ -61,9 +61,6 @@ const MyItem: React.FC<MyItemProps> = (
6161
}}
6262
>
6363
{({ className, style }, motionRef) => {
64-
// if (uuid >= 100) {
65-
// console.log('=>', id, className, style);
66-
// }
6764
return (
6865
<div ref={motionRef} className={classNames('item', className)} style={style} data-id={id}>
6966
<div style={{ height: uuid % 2 ? 100 : undefined }}>
@@ -152,14 +149,16 @@ const Demo = () => {
152149
data={data}
153150
data-id="list"
154151
height={200}
155-
itemHeight={30}
152+
itemHeight={20}
156153
itemKey="id"
157154
disabled={animating}
158155
ref={listRef}
159156
style={{
160157
border: '1px solid red',
161158
boxSizing: 'border-box',
162159
}}
160+
161+
onSkipRender={onAppear}
163162
>
164163
{(item, index) => (
165164
<ForwardMyItem

src/List.tsx

Lines changed: 137 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {
1010
getItemRelativeTop,
1111
getCompareItemRelativeTop,
1212
alignScrollTop,
13+
requireVirtual,
1314
} from './utils/itemUtil';
1415
import { getIndexByStartLoc, findListDiffIndex } from './utils/algorithmUtil';
1516

@@ -45,10 +46,15 @@ export interface ListProps<T> extends React.HTMLAttributes<any> {
4546
itemKey: string;
4647
component?: string | React.FC<any> | React.ComponentClass<any>;
4748
disabled?: boolean;
49+
50+
/** When `disabled`, trigger if changed item not render. */
51+
onSkipRender?: () => void;
4852
}
4953

54+
type Status = 'NONE' | 'MEASURE_START' | 'MEASURE_DONE' | 'SWITCH_TO_VIRTUAL' | 'SWITCH_TO_RAW';
55+
5056
interface ListState<T> {
51-
status: 'NONE' | 'MEASURE_START' | 'MEASURE_DONE';
57+
status: Status;
5258

5359
scrollTop: number | null;
5460
/** Located item index */
@@ -63,6 +69,15 @@ interface ListState<T> {
6369
* we need revert back to the located item index.
6470
*/
6571
startItemTop: number;
72+
73+
/**
74+
* Tell if is using virtual scroll
75+
*/
76+
isVirtual: boolean;
77+
/**
78+
* Only used when turn virtual list to raw list
79+
*/
80+
cacheScroll?: RelativeScroll;
6681
}
6782

6883
/**
@@ -92,16 +107,6 @@ class List<T> extends React.Component<ListProps<T>, ListState<T>> {
92107
data: [],
93108
};
94109

95-
state: ListState<T> = {
96-
status: 'NONE',
97-
scrollTop: null,
98-
itemIndex: 0,
99-
itemOffsetPtg: 0,
100-
startIndex: 0,
101-
endIndex: 0,
102-
startItemTop: 0,
103-
};
104-
105110
listRef = React.createRef<HTMLElement>();
106111

107112
itemElements: { [index: number]: HTMLElement } = {};
@@ -123,6 +128,17 @@ class List<T> extends React.Component<ListProps<T>, ListState<T>> {
123128
super(props);
124129

125130
this.cachedProps = props;
131+
132+
this.state = {
133+
status: 'NONE',
134+
scrollTop: null,
135+
itemIndex: 0,
136+
itemOffsetPtg: 0,
137+
startIndex: 0,
138+
endIndex: 0,
139+
startItemTop: 0,
140+
isVirtual: requireVirtual(props.height, props.itemHeight, props.data.length),
141+
};
126142
}
127143

128144
/**
@@ -141,13 +157,41 @@ class List<T> extends React.Component<ListProps<T>, ListState<T>> {
141157
*/
142158
public componentDidUpdate() {
143159
const { status } = this.state;
144-
const { data, height, itemHeight, disabled } = this.props;
160+
const { data, height, itemHeight, disabled, onSkipRender } = this.props;
145161
const prevData: T[] = this.cachedProps.data || [];
146162

147-
if (disabled || !this.listRef.current) {
163+
const changedItemIndex: number =
164+
prevData.length !== data.length ? findListDiffIndex(prevData, data, this.getItemKey) : null;
165+
166+
if (disabled) {
167+
// Should trigger `onSkipRender` to tell that diff component is not render in the list
168+
if (data.length > prevData.length) {
169+
const { startIndex, endIndex } = this.state;
170+
if (onSkipRender && (changedItemIndex < startIndex || endIndex < changedItemIndex)) {
171+
onSkipRender();
172+
}
173+
}
148174
return;
149175
}
150176

177+
const isVirtual = requireVirtual(height, itemHeight, data.length);
178+
let nextStatus = status;
179+
if (this.state.isVirtual !== isVirtual) {
180+
nextStatus = isVirtual ? 'SWITCH_TO_VIRTUAL' : 'SWITCH_TO_RAW';
181+
this.setState({
182+
isVirtual,
183+
status: nextStatus,
184+
});
185+
186+
/**
187+
* We will wait a tick to let list turn to virtual list.
188+
* And then use virtual list sync logic to adjust the scroll.
189+
*/
190+
if (nextStatus === 'SWITCH_TO_VIRTUAL') {
191+
return;
192+
}
193+
}
194+
151195
if (status === 'MEASURE_START') {
152196
const { startIndex, itemIndex, itemOffsetPtg } = this.state;
153197
const { scrollTop } = this.listRef.current;
@@ -177,13 +221,38 @@ class List<T> extends React.Component<ListProps<T>, ListState<T>> {
177221
});
178222
}
179223

180-
/**
181-
* Re-calculate the item position since `data` length changed.
182-
* [IMPORTANT] We use relative position calculate here.
183-
*/
184-
if (prevData.length !== data.length && height) {
224+
if (status === 'SWITCH_TO_RAW') {
225+
/**
226+
* After virtual list back to raw list,
227+
* we update the `scrollTop` to real top instead of percentage top.
228+
*/
229+
const {
230+
cacheScroll: { itemIndex, relativeTop },
231+
} = this.state;
232+
let rawTop = relativeTop;
233+
for (let index = 0; index < itemIndex; index += 1) {
234+
rawTop -= this.itemElementHeights[this.getIndexKey(index)] || 0;
235+
}
236+
237+
this.lockScroll = true;
238+
this.listRef.current.scrollTop = -rawTop;
239+
240+
this.setState({
241+
status: 'MEASURE_DONE',
242+
});
243+
244+
requestAnimationFrame(() => {
245+
requestAnimationFrame(() => {
246+
this.lockScroll = false;
247+
});
248+
});
249+
} else if (prevData.length !== data.length && height) {
250+
/**
251+
* Re-calculate the item position since `data` length changed.
252+
* [IMPORTANT] We use relative position calculate here.
253+
*/
254+
let { itemIndex: originItemIndex } = this.state;
185255
const {
186-
itemIndex: originItemIndex,
187256
itemOffsetPtg: originItemOffsetPtg,
188257
startIndex: originStartIndex,
189258
endIndex: originEndIndex,
@@ -194,21 +263,27 @@ class List<T> extends React.Component<ListProps<T>, ListState<T>> {
194263
this.collectItemHeights();
195264

196265
// 1. Get origin located item top
197-
const originLocatedItemRelativeTop = getItemRelativeTop({
198-
itemIndex: originItemIndex,
199-
itemOffsetPtg: originItemOffsetPtg,
200-
itemElementHeights: this.itemElementHeights,
201-
scrollPtg: getScrollPercentage({
202-
scrollTop: originScrollTop,
203-
scrollHeight: prevData.length * itemHeight,
266+
let originLocatedItemRelativeTop: number;
267+
268+
if (this.state.status === 'SWITCH_TO_VIRTUAL') {
269+
originItemIndex = 0;
270+
originLocatedItemRelativeTop = -this.state.scrollTop;
271+
} else {
272+
originLocatedItemRelativeTop = getItemRelativeTop({
273+
itemIndex: originItemIndex,
274+
itemOffsetPtg: originItemOffsetPtg,
275+
itemElementHeights: this.itemElementHeights,
276+
scrollPtg: getScrollPercentage({
277+
scrollTop: originScrollTop,
278+
scrollHeight: prevData.length * itemHeight,
279+
clientHeight: this.listRef.current.clientHeight,
280+
}),
204281
clientHeight: this.listRef.current.clientHeight,
205-
}),
206-
clientHeight: this.listRef.current.clientHeight,
207-
getItemKey: (index: number) => this.getIndexKey(index, this.cachedProps),
208-
});
282+
getItemKey: (index: number) => this.getIndexKey(index, this.cachedProps),
283+
});
284+
}
209285

210286
// 2. Find the compare item
211-
const changedItemIndex: number = findListDiffIndex(prevData, data, this.getItemKey);
212287
let originCompareItemIndex = changedItemIndex - 1;
213288
// Use next one since there are not more item before removed
214289
if (originCompareItemIndex < 0) {
@@ -226,10 +301,22 @@ class List<T> extends React.Component<ListProps<T>, ListState<T>> {
226301
itemElementHeights: this.itemElementHeights,
227302
});
228303

229-
this.internalScrollTo({
230-
itemIndex: originCompareItemIndex,
231-
relativeTop: originCompareItemTop,
232-
});
304+
if (nextStatus === 'SWITCH_TO_RAW') {
305+
/**
306+
* We will record current measure relative item top and apply in raw list after list turned
307+
*/
308+
this.setState({
309+
cacheScroll: {
310+
itemIndex: originCompareItemIndex,
311+
relativeTop: originCompareItemTop,
312+
},
313+
});
314+
} else {
315+
this.internalScrollTo({
316+
itemIndex: originCompareItemIndex,
317+
relativeTop: originCompareItemTop,
318+
});
319+
}
233320
}
234321

235322
this.cachedProps = this.props;
@@ -268,6 +355,12 @@ class List<T> extends React.Component<ListProps<T>, ListState<T>> {
268355
});
269356
};
270357

358+
public onRawScroll = () => {
359+
const { scrollTop } = this.listRef.current;
360+
361+
this.setState({ scrollTop });
362+
};
363+
271364
public getIndexKey = (index: number, props?: Partial<ListProps<T>>) => {
272365
const mergedProps = props || this.props;
273366
const { data = [] } = mergedProps;
@@ -431,6 +524,7 @@ class List<T> extends React.Component<ListProps<T>, ListState<T>> {
431524
};
432525

433526
public render() {
527+
const { isVirtual } = this.state;
434528
const {
435529
style,
436530
component: Component = 'div',
@@ -439,13 +533,19 @@ class List<T> extends React.Component<ListProps<T>, ListState<T>> {
439533
data,
440534
children,
441535
itemKey,
536+
onSkipRender,
442537
...restProps
443538
} = this.props;
444539

445540
// Render pure list if not set height or height is enough for all items
446-
if (typeof height !== 'number' || data.length * itemHeight <= height) {
541+
if (!isVirtual) {
447542
return (
448-
<Component style={height ? { ...style, height, ...ScrollStyle } : style} {...restProps}>
543+
<Component
544+
style={height ? { ...style, height, ...ScrollStyle } : style}
545+
{...restProps}
546+
onScroll={this.onRawScroll}
547+
ref={this.listRef}
548+
>
449549
<Filler height={height}>{this.renderChildren(data, 0, children)}</Filler>
450550
</Component>
451551
);

src/utils/itemUtil.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -184,3 +184,7 @@ export function getCompareItemRelativeTop({
184184

185185
return originCompareItemTop;
186186
}
187+
188+
export function requireVirtual(height: number, itemHeight: number, count: number) {
189+
return typeof height === 'number' && count * itemHeight > height;
190+
}

0 commit comments

Comments
 (0)