Skip to content

Commit 903cf32

Browse files
committed
optimze find diff logic
1 parent 9dafe02 commit 903cf32

File tree

7 files changed

+157
-25
lines changed

7 files changed

+157
-25
lines changed

examples/animate.tsx

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

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

@@ -116,6 +116,7 @@ const Demo = () => {
116116
<React.StrictMode>
117117
<div>
118118
<h2>Animate</h2>
119+
<p>Current: {dataSource.length} records</p>
119120

120121
<List
121122
dataSource={dataSource}

examples/basic.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ const MyItem: React.FC<Item> = ({ id }, ref) => {
2525

2626
const ForwardMyItem = React.forwardRef(MyItem);
2727

28-
class TestItem extends React.Component {
28+
class TestItem extends React.Component<{ id: number }> {
2929
render() {
3030
return <div style={{ lineHeight: '30px' }}>{this.props.id}</div>;
3131
}
@@ -68,6 +68,7 @@ const Demo = () => {
6868
dataSource={dataSource}
6969
height={200}
7070
itemHeight={30}
71+
itemKey="id"
7172
style={{
7273
border: '1px solid red',
7374
boxSizing: 'border-box',

src/List.tsx

Lines changed: 29 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,8 @@ import {
99
GHOST_ITEM_KEY,
1010
getItemRelativeTop,
1111
getCompareItemRelativeTop,
12-
} from './util';
12+
} from './utils/itemUtil';
13+
import { getIndexByStartLoc, findListDiffIndex } from './utils/algorithmUtil';
1314

1415
type RenderFunc<T> = (item: T) => React.ReactNode;
1516

@@ -20,7 +21,7 @@ export interface ListProps<T> extends React.HTMLAttributes<any> {
2021
dataSource: T[];
2122
height?: number;
2223
itemHeight?: number;
23-
itemKey?: string;
24+
itemKey: string;
2425
component?: string | React.FC<any> | React.ComponentClass<any>;
2526
}
2627

@@ -112,7 +113,7 @@ class List<T> extends React.Component<ListProps<T>, ListState> {
112113

113114
// Record here since measure item height will get warning in `render`
114115
for (let index = startIndex; index <= endIndex; index += 1) {
115-
const eleKey = this.getItemKey(index);
116+
const eleKey = this.getIndexKey(index);
116117
this.itemElementHeights[eleKey] = getNodeHeight(this.itemElements[eleKey]);
117118
}
118119

@@ -124,12 +125,12 @@ class List<T> extends React.Component<ListProps<T>, ListState> {
124125
scrollTop: this.listRef.current.scrollTop,
125126
scrollPtg: getElementScrollPercentage(this.listRef.current),
126127
clientHeight: this.listRef.current.clientHeight,
127-
getItemKey: this.getItemKey,
128+
getItemKey: this.getIndexKey,
128129
});
129130

130131
let startItemTop = locatedItemTop;
131132
for (let index = itemIndex - 1; index >= startIndex; index -= 1) {
132-
startItemTop -= this.itemElementHeights[this.getItemKey(index)] || 0;
133+
startItemTop -= this.itemElementHeights[this.getIndexKey(index)] || 0;
133134
}
134135

135136
this.setState({ status: 'MEASURE_DONE', startItemTop });
@@ -159,14 +160,15 @@ class List<T> extends React.Component<ListProps<T>, ListState> {
159160
clientHeight: this.listRef.current.clientHeight,
160161
}),
161162
clientHeight: this.listRef.current.clientHeight,
162-
getItemKey: (index: number) => this.getItemKey(index, prevProps),
163+
getItemKey: (index: number) => this.getIndexKey(index, prevProps),
163164
});
164165

165166
// 2. Find the compare item
166-
const removedItemIndex: number = prevProps.dataSource.findIndex((_, index) => {
167-
const key = this.getItemKey(index, prevProps);
168-
return dataSource.every((__, nextIndex) => key !== this.getItemKey(nextIndex));
169-
});
167+
const removedItemIndex: number = findListDiffIndex(
168+
prevProps.dataSource,
169+
dataSource,
170+
this.getItemKey,
171+
);
170172
let originCompareItemIndex = removedItemIndex - 1;
171173
// Use next one since there are not more item before removed
172174
if (originCompareItemIndex < 0) {
@@ -180,7 +182,7 @@ class List<T> extends React.Component<ListProps<T>, ListState> {
180182
compareItemIndex: originCompareItemIndex,
181183
startIndex: originStartIndex,
182184
endIndex: originEndIndex,
183-
getItemKey: (index: number) => this.getItemKey(index, prevProps),
185+
getItemKey: (index: number) => this.getIndexKey(index, prevProps),
184186
itemElementHeights: this.itemElementHeights,
185187
});
186188

@@ -195,7 +197,9 @@ class List<T> extends React.Component<ListProps<T>, ListState> {
195197
const scrollHeight = dataSource.length * itemHeight;
196198
const { clientHeight } = this.listRef.current;
197199
const maxScrollTop = scrollHeight - clientHeight;
198-
for (let scrollTop = 0; scrollTop < maxScrollTop; scrollTop += 1) {
200+
for (let i = 0; i < maxScrollTop; i += 1) {
201+
const scrollTop = getIndexByStartLoc(0, maxScrollTop, originScrollTop, i);
202+
199203
const scrollPtg = getScrollPercentage({ scrollTop, scrollHeight, clientHeight });
200204
const visibleCount = Math.ceil(height / itemHeight);
201205

@@ -214,7 +218,7 @@ class List<T> extends React.Component<ListProps<T>, ListState> {
214218
itemElementHeights: this.itemElementHeights,
215219
scrollPtg,
216220
clientHeight,
217-
getItemKey: this.getItemKey,
221+
getItemKey: this.getIndexKey,
218222
});
219223

220224
const compareItemTop = getCompareItemRelativeTop({
@@ -223,7 +227,7 @@ class List<T> extends React.Component<ListProps<T>, ListState> {
223227
compareItemIndex: originCompareItemIndex, // Same as origin index
224228
startIndex,
225229
endIndex,
226-
getItemKey: this.getItemKey,
230+
getItemKey: this.getIndexKey,
227231
itemElementHeights: this.itemElementHeights,
228232
});
229233

@@ -295,8 +299,9 @@ class List<T> extends React.Component<ListProps<T>, ListState> {
295299
});
296300
};
297301

298-
public getItemKey = (index: number, props?: ListProps<T>) => {
299-
const { dataSource, itemKey } = props || this.props;
302+
public getIndexKey = (index: number, props?: ListProps<T>) => {
303+
const mergedProps = props || this.props;
304+
const { dataSource } = mergedProps;
300305

301306
// Return ghost key as latest index item
302307
if (index === dataSource.length) {
@@ -307,7 +312,13 @@ class List<T> extends React.Component<ListProps<T>, ListState> {
307312
if (!item) {
308313
console.error('Not find index item. Please report this since it is a bug.');
309314
}
310-
return item && itemKey ? item[itemKey] : index;
315+
316+
return this.getItemKey(item, mergedProps);
317+
};
318+
319+
public getItemKey = (item: T, props?: ListProps<T>) => {
320+
const { itemKey } = props || this.props;
321+
return item ? item[itemKey] : null;
311322
};
312323

313324
/**
@@ -318,7 +329,7 @@ class List<T> extends React.Component<ListProps<T>, ListState> {
318329
list.map((item, index) => {
319330
const node = renderFunc(item) as React.ReactElement;
320331
const eleIndex = startIndex + index;
321-
const eleKey = this.getItemKey(eleIndex);
332+
const eleKey = this.getIndexKey(eleIndex);
322333

323334
// Pass `key` and `ref` for internal measure
324335
return React.cloneElement(node, {

src/utils/algorithmUtil.ts

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
/**
2+
* Get index with specific start index one by one. e.g.
3+
* min: 3, max: 9, start: 6
4+
*
5+
* Return index is:
6+
* [0]: 6
7+
* [1]: 7
8+
* [2]: 5
9+
* [3]: 8
10+
* [4]: 4
11+
* [5]: 9
12+
* [6]: 3
13+
*/
14+
export function getIndexByStartLoc(min: number, max: number, start: number, index: number): number {
15+
const beforeCount = start - min;
16+
const afterCount = max - start;
17+
const balanceCount = Math.min(beforeCount, afterCount) * 2;
18+
19+
// Balance
20+
if (index <= balanceCount) {
21+
const stepIndex = Math.floor(index / 2);
22+
if (index % 2) {
23+
return start + stepIndex + 1;
24+
}
25+
return start - stepIndex;
26+
}
27+
28+
// One is out of range
29+
if (beforeCount > afterCount) {
30+
return start - (index - afterCount);
31+
}
32+
return start + (index - beforeCount);
33+
}
34+
35+
/**
36+
* We assume that 2 list has only 1 item diff and others keeping the order.
37+
* So we can use dichotomy algorithm to find changed one.
38+
*/
39+
export function findListDiffIndex<T>(
40+
originList: T[],
41+
targetList: T[],
42+
getKey: (item: T) => string,
43+
): number | null {
44+
if (originList.length === targetList.length) {
45+
return null;
46+
}
47+
48+
let startIndex = 0;
49+
let endIndex = originList.length - 1;
50+
let midIndex = Math.floor((startIndex + endIndex) / 2);
51+
52+
const keyCache: Map<T, string | { __EMPTY_ITEM__: true }> = new Map();
53+
54+
function getCacheKey(item: T) {
55+
if (!keyCache.has(item)) {
56+
keyCache.set(item, item !== undefined ? getKey(item) : { __EMPTY_ITEM__: true });
57+
}
58+
return keyCache.get(item);
59+
}
60+
61+
while (startIndex !== midIndex || midIndex !== endIndex) {
62+
const originMidKey = getCacheKey(originList[midIndex]);
63+
const targetMidKey = getCacheKey(targetList[midIndex]);
64+
65+
if (originMidKey === targetMidKey) {
66+
startIndex = midIndex;
67+
} else {
68+
endIndex = midIndex;
69+
}
70+
71+
// Check if there only 2 index left
72+
if (startIndex === endIndex - 1) {
73+
return getCacheKey(originList[startIndex]) !== getCacheKey(targetList[startIndex])
74+
? startIndex
75+
: endIndex;
76+
}
77+
78+
midIndex = Math.floor((startIndex + endIndex) / 2);
79+
}
80+
81+
return midIndex;
82+
}
File renamed without changes.

tests/index.test.js

Lines changed: 0 additions & 5 deletions
This file was deleted.

tests/util.test.js

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import { getIndexByStartLoc, findListDiffIndex } from '../src/utils/algorithmUtil';
2+
3+
describe('Util', () => {
4+
describe('getIndexByStartLoc', () => {
5+
function test(name, min, max, start, expectList) {
6+
it(name, () => {
7+
const len = max - min + 1;
8+
const renderList = new Array(len)
9+
.fill(null)
10+
.map((_, index) => getIndexByStartLoc(min, max, start, index));
11+
12+
expect(renderList).toEqual(expectList);
13+
});
14+
}
15+
16+
// Balance
17+
test('balance - basic', 0, 2, 1, [1, 2, 0]);
18+
test('balance - moving', 3, 13, 8, [8, 9, 7, 10, 6, 11, 5, 12, 4, 13, 3]);
19+
20+
// After less
21+
test('after less', 3, 9, 7, [7, 8, 6, 9, 5, 4, 3]);
22+
23+
// Before less
24+
test('before less', 1, 9, 3, [3, 4, 2, 5, 1, 6, 7, 8, 9]);
25+
});
26+
27+
describe('findListDiff', () => {
28+
function test(name, length, diff) {
29+
it(name, () => {
30+
const originList = new Array(length).fill(null).map((_, index) => index);
31+
const targetList = originList.slice();
32+
targetList.splice(diff, 1);
33+
34+
expect(findListDiffIndex(originList, targetList, num => num)).toEqual(diff);
35+
});
36+
}
37+
38+
for (let i = 0; i < 100; i += 1) {
39+
test(`diff index: ${i}`, 100, i);
40+
}
41+
});
42+
});

0 commit comments

Comments
 (0)