Skip to content

Commit e252fb4

Browse files
committed
perf(ui:virtual-scroll): optimize performance
1 parent 5394fe7 commit e252fb4

1 file changed

Lines changed: 94 additions & 89 deletions

File tree

packages/ui/src/components/_virtual-scroll/VirtualScroll.tsx

Lines changed: 94 additions & 89 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
import { isArray } from 'lodash';
2-
import React, { useCallback, useEffect, useLayoutEffect, useMemo, useRef } from 'react';
2+
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
33
import { flushSync } from 'react-dom';
44

5-
import { useAsync, useImmer, useRefCallback } from '../../hooks';
5+
import { useAsync, useRefCallback } from '../../hooks';
66

77
export interface DItemRenderProps {
88
'aria-setsize'?: number;
@@ -54,18 +54,15 @@ export function DVirtualScroll<T>(props: DVirtualScrollProps<T>) {
5454
//#endregion
5555

5656
const dataRef = useRef<{
57-
isFirst: boolean;
57+
hasScrollChange: boolean;
5858
hasInitFocus: boolean;
5959
}>({
60-
isFirst: true,
60+
hasScrollChange: false,
6161
hasInitFocus: dHasSelected,
6262
});
6363

6464
const asyncCapture = useAsync();
6565

66-
const [list, setList] = useImmer<React.ReactNode[]>([]);
67-
const [fillSize, setFillSize] = useImmer<[React.CSSProperties, React.CSSProperties]>([{}, {}]);
68-
6966
const [flatOptions, focusIndex] = useMemo(() => {
7067
const flatOptions: Array<T | undefined> = [];
7168
let focusIndex = -1;
@@ -101,76 +98,77 @@ export function DVirtualScroll<T>(props: DVirtualScrollProps<T>) {
10198
return [flatOptions, hasFind ? focusIndex : -1];
10299
}, [dCompareOption, dEmpty, dList, dNestedKey, dFocusOption]);
103100

104-
const updateList = useCallback(() => {
105-
if (listEl) {
106-
dataRef.current.isFirst = false;
107-
108-
const maxScrollSize = dItemSize * flatOptions.length + dPaddingSize * 2 - dSize;
109-
const scrollSize = Math.min(maxScrollSize, dScrollY ? listEl.scrollTop : listEl.scrollLeft);
110-
111-
const startCount = Math.floor((scrollSize - dPaddingSize) / dItemSize) - 2;
112-
const endCount = Math.ceil((scrollSize - dPaddingSize + dSize) / dItemSize) + 2;
113-
114-
let count = 0;
115-
let skipCount = 0;
116-
let renderCount = 0;
117-
const loop = (arr: T[]) => {
118-
const list: React.ReactNode[] = [];
119-
if (arr.length === 0) {
120-
if (dEmpty) {
121-
count += 1;
122-
if (count > endCount) {
123-
return list;
124-
}
125-
const shouldRender = count > startCount;
126-
if (shouldRender) {
101+
const getStates = useCallback(() => {
102+
const maxScrollSize = dItemSize * flatOptions.length + dPaddingSize * 2 - dSize;
103+
const scrollSize = Math.min(maxScrollSize, dScrollY ? listEl?.scrollTop ?? 0 : listEl?.scrollLeft ?? 0);
104+
105+
const startCount = Math.floor((scrollSize - dPaddingSize) / dItemSize) - 2;
106+
const endCount = Math.ceil((scrollSize - dPaddingSize + dSize) / dItemSize) + 2;
107+
108+
let count = 0;
109+
let skipCount = 0;
110+
let renderCount = 0;
111+
const loop = (arr: T[]) => {
112+
const list: React.ReactNode[] = [];
113+
if (arr.length === 0) {
114+
if (dEmpty) {
115+
count += 1;
116+
if (count > endCount) {
117+
return list;
118+
}
119+
const shouldRender = count > startCount;
120+
if (shouldRender) {
121+
renderCount += 1;
122+
list.push(dEmpty);
123+
} else {
124+
skipCount += 1;
125+
}
126+
}
127+
} else {
128+
for (let index = 0; index < arr.length; index++) {
129+
count += 1;
130+
if (count > endCount) {
131+
return list;
132+
}
133+
const shouldRender = count > startCount;
134+
if (dNestedKey && isArray(arr[index][dNestedKey])) {
135+
const children = loop(arr[index][dNestedKey] as T[]);
136+
if (shouldRender || children.length > 0) {
127137
renderCount += 1;
128-
list.push(dEmpty);
138+
list.push(dItemRender(arr[index], { children }));
129139
} else {
130140
skipCount += 1;
131141
}
132-
}
133-
} else {
134-
for (let index = 0; index < arr.length; index++) {
135-
count += 1;
136-
if (count > endCount) {
137-
return list;
138-
}
139-
const shouldRender = count > startCount;
140-
if (dNestedKey && isArray(arr[index][dNestedKey])) {
141-
const children = loop(arr[index][dNestedKey] as T[]);
142-
if (shouldRender || children.length > 0) {
143-
renderCount += 1;
144-
list.push(dItemRender(arr[index], { children }));
145-
} else {
146-
skipCount += 1;
147-
}
142+
} else {
143+
if (shouldRender) {
144+
renderCount += 1;
145+
list.push(
146+
dItemRender(arr[index], {
147+
'aria-setsize': arr.length,
148+
'aria-posinset': index + 1,
149+
})
150+
);
148151
} else {
149-
if (shouldRender) {
150-
renderCount += 1;
151-
list.push(
152-
dItemRender(arr[index], {
153-
'aria-setsize': arr.length,
154-
'aria-posinset': index + 1,
155-
})
156-
);
157-
} else {
158-
skipCount += 1;
159-
}
152+
skipCount += 1;
160153
}
161154
}
162155
}
163-
return list;
164-
};
165-
166-
setList(loop(dList));
156+
}
157+
return list;
158+
};
167159

168-
setFillSize([
160+
return {
161+
list: loop(dList),
162+
fillSize: [
169163
{ [dScrollY ? 'height' : 'width']: dItemSize * skipCount },
170164
{ [dScrollY ? 'height' : 'width']: dItemSize * (flatOptions.length - skipCount - renderCount) },
171-
]);
172-
}
173-
}, [dEmpty, dItemRender, dItemSize, dList, dNestedKey, dPaddingSize, dScrollY, dSize, flatOptions.length, listEl, setFillSize, setList]);
165+
],
166+
};
167+
}, [dEmpty, dItemRender, dItemSize, dList, dNestedKey, dPaddingSize, dScrollY, dSize, flatOptions.length, listEl]);
168+
const [{ list, fillSize }, _updateList] = useState(() => getStates());
169+
const updateList = useCallback(() => {
170+
_updateList(getStates());
171+
}, [getStates]);
174172

175173
const handleScroll = useCallback(
176174
(e) => {
@@ -186,44 +184,47 @@ export function DVirtualScroll<T>(props: DVirtualScrollProps<T>) {
186184
}
187185

188186
flushSync(() => updateList());
187+
dataRef.current.hasScrollChange = false;
189188
},
190189
[dScrollY, listEl, onScroll, onScrollEnd, updateList]
191190
);
192191

193-
useLayoutEffect(() => {
194-
if (!dataRef.current.isFirst) {
195-
if (dRendered) {
196-
if (dataRef.current.hasInitFocus && listEl) {
197-
dataRef.current.hasInitFocus = false;
198-
listEl[dScrollY ? 'scrollTop' : 'scrollLeft'] = focusIndex * dItemSize;
199-
} else {
192+
useEffect(() => {
193+
if (dRendered) {
194+
if (dataRef.current.hasInitFocus && listEl) {
195+
dataRef.current.hasInitFocus = false;
196+
listEl[dScrollY ? 'scrollTop' : 'scrollLeft'] = focusIndex * dItemSize;
197+
} else {
198+
if (!dataRef.current.hasScrollChange) {
200199
updateList();
201200
}
201+
dataRef.current.hasScrollChange = false;
202202
}
203203
}
204204
// eslint-disable-next-line react-hooks/exhaustive-deps
205205
}, [dRendered, updateList]);
206206

207-
useLayoutEffect(() => {
208-
if (dataRef.current.isFirst) {
209-
updateList();
210-
}
211-
}, [updateList]);
212-
213207
useEffect(() => {
214208
const [asyncGroup, asyncId] = asyncCapture.createGroup();
215209

216210
if (listEl && dRendered && focusIndex !== -1) {
211+
const changeScroll = (num: number) => {
212+
const pre = listEl[dScrollY ? 'scrollTop' : 'scrollLeft'];
213+
listEl[dScrollY ? 'scrollTop' : 'scrollLeft'] = num;
214+
const now = listEl[dScrollY ? 'scrollTop' : 'scrollLeft'];
215+
dataRef.current.hasScrollChange = pre !== now;
216+
};
217+
217218
const changeFocusByKeydown = (next = true) => {
218219
let index = focusIndex;
219220
let option: T | undefined;
220221
const getOption = () => {
221222
if (!next && index === 0) {
222-
listEl[dScrollY ? 'scrollTop' : 'scrollLeft'] = 0;
223+
changeScroll(0);
223224
return;
224225
}
225226
if (next && index === flatOptions.length - 1) {
226-
listEl[dScrollY ? 'scrollTop' : 'scrollLeft'] = listEl[dScrollY ? 'scrollHeight' : 'scrollWidth'];
227+
changeScroll(listEl[dScrollY ? 'scrollHeight' : 'scrollWidth']);
227228
return;
228229
}
229230
index = next ? index + 1 : index - 1;
@@ -242,17 +243,17 @@ export function DVirtualScroll<T>(props: DVirtualScrollProps<T>) {
242243
const listElClientSize = listEl[dScrollY ? 'clientHeight' : 'clientWidth'];
243244

244245
if (listElScrollSize > elOffset[1]) {
245-
listEl[dScrollY ? 'scrollTop' : 'scrollLeft'] = elOffset[0] - dPaddingSize;
246+
changeScroll(elOffset[0] - dPaddingSize);
246247
} else if (elOffset[0] > listElScrollSize + listElClientSize) {
247-
listEl[dScrollY ? 'scrollTop' : 'scrollLeft'] = elOffset[1] - listElClientSize + dPaddingSize;
248+
changeScroll(elOffset[1] - listElClientSize + dPaddingSize);
248249
} else {
249250
if (next) {
250251
if (elOffset[1] > listElScrollSize + listElClientSize) {
251-
listEl[dScrollY ? 'scrollTop' : 'scrollLeft'] = elOffset[1] - listElClientSize + dPaddingSize;
252+
changeScroll(elOffset[1] - listElClientSize + dPaddingSize);
252253
}
253254
} else {
254255
if (listElScrollSize > elOffset[0]) {
255-
listEl[dScrollY ? 'scrollTop' : 'scrollLeft'] = elOffset[0] - dPaddingSize;
256+
changeScroll(elOffset[0] - dPaddingSize);
256257
}
257258
}
258259
}
@@ -263,6 +264,8 @@ export function DVirtualScroll<T>(props: DVirtualScrollProps<T>) {
263264

264265
asyncGroup.fromEvent<KeyboardEvent>(window, 'keydown').subscribe({
265266
next: (e) => {
267+
let option: T | undefined;
268+
266269
switch (e.code) {
267270
case 'ArrowUp':
268271
e.preventDefault();
@@ -294,25 +297,27 @@ export function DVirtualScroll<T>(props: DVirtualScrollProps<T>) {
294297

295298
case 'Home':
296299
e.preventDefault();
297-
listEl[dScrollY ? 'scrollTop' : 'scrollLeft'] = 0;
300+
changeScroll(0);
298301
for (const item of flatOptions) {
299302
if (item && dCanSelectOption(item)) {
300-
onFocusChange?.(item);
303+
option = item;
301304
break;
302305
}
303306
}
307+
onFocusChange?.(option ?? null);
304308
break;
305309

306310
case 'End':
307311
e.preventDefault();
308-
listEl[dScrollY ? 'scrollTop' : 'scrollLeft'] = listEl[dScrollY ? 'scrollHeight' : 'scrollWidth'];
312+
changeScroll(listEl[dScrollY ? 'scrollHeight' : 'scrollWidth']);
309313
for (let index = flatOptions.length - 1; index >= 0; index--) {
310314
const item = flatOptions[index];
311315
if (item && dCanSelectOption(item)) {
312-
onFocusChange?.(item);
316+
option = item;
313317
break;
314318
}
315319
}
320+
onFocusChange?.(option ?? null);
316321
break;
317322

318323
default:

0 commit comments

Comments
 (0)