Skip to content

Commit 542f220

Browse files
committed
perf(ui): optimizing handle of big data
1 parent ae97c22 commit 542f220

19 files changed

Lines changed: 251 additions & 405 deletions

File tree

packages/site/src/app/components/layout/header/Header.scss

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,7 @@
9494
min-width: 36px;
9595
height: 36px;
9696
margin: 0;
97-
font-family: inherit;
97+
font: inherit;
9898
color: var(--d-text-color);
9999
text-align: unset;
100100
text-transform: none;

packages/ui/src/components/auto-complete/AutoComplete.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import type { DFocusVisibleRenderProps } from '../_focus-visible';
33
import type { DVirtualScrollRef } from '../_virtual-scroll';
44

55
import { isUndefined } from 'lodash';
6-
import React, { useState, useId, useCallback, useMemo, useRef, useImperativeHandle, useEffect } from 'react';
6+
import React, { useState, useId, useCallback, useRef, useImperativeHandle, useEffect } from 'react';
77
import ReactDOM from 'react-dom';
88

99
import {
@@ -125,13 +125,13 @@ function AutoComplete<T extends DAutoCompleteOption>(
125125
useUpdatePosition(updatePosition, visible);
126126

127127
const [_focusOption, setFocusOption] = useState<DNestedChildren<T> | undefined>();
128-
const focusOption = useMemo(() => {
128+
const focusOption = (() => {
129129
if (_focusOption && findNested(dOptions, (o) => canSelectOption(o) && o.value === _focusOption.value)) {
130130
return _focusOption;
131131
}
132132

133133
return findNested(dOptions, (o) => canSelectOption(o));
134-
}, [_focusOption, canSelectOption, dOptions]);
134+
})();
135135

136136
const changeFocusOption = (option?: DNestedChildren<T>) => {
137137
if (!isUndefined(option)) {
@@ -371,7 +371,7 @@ function AutoComplete<T extends DAutoCompleteOption>(
371371
style={{ paddingLeft: parent.length === 0 ? undefined : 12 + 8 }}
372372
title={optionValue}
373373
role="option"
374-
aria-auto-completeed={false}
374+
aria-selected={false}
375375
aria-disabled={optionDisabled}
376376
onClick={() => {
377377
if (!optionDisabled) {

packages/ui/src/components/cascader/Cascader.tsx

Lines changed: 72 additions & 120 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@ import type { DFormControl } from '../form';
44
import type { DSelectOption } from '../select';
55
import type { AbstractTreeNode } from '../tree';
66

7-
import { isArray, isNull } from 'lodash';
8-
import React, { useCallback, useMemo, useState, useId, useRef } from 'react';
7+
import { isNull } from 'lodash';
8+
import React, { useCallback, useState, useId, useMemo } from 'react';
99

1010
import { usePrefixConfig, useComponentConfig, useGeneralContext, useDValue, useEventNotify } from '../../hooks';
1111
import { LoadingOutlined } from '../../icons';
@@ -14,8 +14,7 @@ import { DSelectbox } from '../_selectbox';
1414
import { DDropdown } from '../dropdown';
1515
import { useFormControl } from '../form';
1616
import { DTag } from '../tag';
17-
import { useTreeData } from '../tree';
18-
import { SingleTreeNode, MultipleTreeNode } from '../tree';
17+
import { MultipleTreeNode, SingleTreeNode } from '../tree';
1918
import { DList } from './List';
2019
import { DSearchList } from './SearchList';
2120
import { getText, TREE_NODE_KEY } from './utils';
@@ -108,86 +107,67 @@ function Cascader<V extends DId, T extends DCascaderOption<V>>(
108107
const listId = `${dPrefix}cascader-list-${uniqueId}`;
109108
const getOptionId = (val: V) => `${dPrefix}cascader-option-${val}-${uniqueId}`;
110109

110+
const renderNodes = useMemo(
111+
() =>
112+
dOptions.map((option) =>
113+
dMultiple
114+
? new MultipleTreeNode(option, (o) => o.value, {
115+
disabled: option.disabled,
116+
})
117+
: new SingleTreeNode(option, (o) => o.value, {
118+
disabled: option.disabled,
119+
})
120+
),
121+
[dMultiple, dOptions]
122+
);
123+
const nodesMap = useMemo(() => {
124+
const nodes = new Map<V, AbstractTreeNode<V, T>>();
125+
const reduceArr = (arr: AbstractTreeNode<V, T>[]) => {
126+
for (const item of arr) {
127+
nodes.set(item.id, item);
128+
if (item.children) {
129+
reduceArr(item.children);
130+
}
131+
}
132+
};
133+
reduceArr(renderNodes);
134+
return nodes;
135+
}, [renderNodes]);
136+
111137
const [searchValue, setSearchValue] = useState('');
112138

113139
const [visible, changeVisible] = useDValue<boolean>(false, dVisible, onVisibleChange);
114140
const formControlInject = useFormControl(dFormControl);
115-
const [select, changeSelect] = useDValue<V | null | V[]>(
141+
const [_select, changeSelect] = useDValue<V | null | V[]>(
116142
dMultiple ? [] : null,
117143
dModel,
118144
(value) => {
119145
if (onModelChange) {
120-
if (isArray(value)) {
121-
let length = value.length;
122-
const options: DNestedChildren<T>[] = [];
123-
const reduceArr = (arr: DNestedChildren<T>[]) => {
124-
for (const item of arr) {
125-
if (length === 0) {
126-
break;
127-
}
128-
if (item.children) {
129-
reduceArr(item.children);
130-
} else {
131-
const index = value.findIndex((val) => val === item.value);
132-
if (index !== -1) {
133-
options[index] = item;
134-
length -= 1;
135-
}
136-
}
137-
}
138-
};
139-
reduceArr(dOptions);
140-
141-
onModelChange(value, options);
146+
if (dMultiple) {
147+
onModelChange(
148+
value,
149+
(value as V[]).map((v) => nodesMap.get(v)?.origin)
150+
);
142151
} else {
143-
if (isNull(value)) {
144-
onModelChange(value, null);
145-
} else {
146-
onModelChange(
147-
value,
148-
findNested(dOptions, (option) => option.value === value)
149-
);
150-
}
152+
onModelChange(value, isNull(value) ? null : nodesMap.get(value as V)?.origin);
151153
}
152154
}
153155
},
154156
undefined,
155157
formControlInject
156158
);
159+
const select = useMemo(() => (dMultiple ? new Set(_select as V[]) : (_select as V | null)), [_select, dMultiple]);
160+
renderNodes.forEach((node) => {
161+
node.updateStatus(select);
162+
});
157163

158164
const size = dSize ?? gSize;
159165
const disabled = dDisabled || gDisabled || dFormControl?.control.disabled;
160166

161167
const hasSearch = searchValue.length > 0;
162-
const hasSelected = dMultiple ? (select as V[]).length > 0 : !isNull(select);
163-
164-
const checkedRef = useRef<SingleTreeNode<V, T>>();
165-
const getRenderNodes = useCallback(
166-
(select: V | null | V[]) => {
167-
let renderNodes: SingleTreeNode<V, T>[] | MultipleTreeNode<V, T>[] = [];
168-
if (dMultiple) {
169-
renderNodes = dOptions.map(
170-
(option) =>
171-
new MultipleTreeNode(option, (o) => o.value, {
172-
checkeds: select as V[],
173-
disabled: option.disabled,
174-
})
175-
);
176-
} else {
177-
renderNodes = dOptions.map(
178-
(option) =>
179-
new SingleTreeNode(option, (o) => o.value, {
180-
checkedRef,
181-
checked: select as V | null,
182-
disabled: option.disabled,
183-
})
184-
);
185-
}
186-
return renderNodes;
187-
},
188-
[dMultiple, dOptions]
189-
);
190-
const [renderNodes, changeSelectByCache] = useTreeData(select, getRenderNodes, changeSelect);
168+
const hasSelected = dMultiple ? (select as Set<V>).size > 0 : !isNull(select);
169+
170+
const [focusVisible, setFocusVisible] = useState(false);
191171

192172
const filterFn = useCallback(
193173
(option: T, searchStr = searchValue) => {
@@ -199,7 +179,7 @@ function Cascader<V extends DId, T extends DCascaderOption<V>>(
199179
[dCustomSearch, searchValue]
200180
);
201181
const sortFn = dCustomSearch?.sort;
202-
const searchOptions = useMemo(() => {
182+
const searchOptions = (() => {
203183
if (!hasSearch) {
204184
return [];
205185
}
@@ -228,40 +208,32 @@ function Cascader<V extends DId, T extends DCascaderOption<V>>(
228208
searchOptions.sort((a, b) => sortFn(a[TREE_NODE_KEY].origin, b[TREE_NODE_KEY].origin));
229209
}
230210
return searchOptions;
231-
}, [dMultiple, dOnlyLeafSelectable, filterFn, hasSearch, renderNodes, sortFn]);
232-
233-
const [focusVisible, setFocusVisible] = useState(false);
211+
})();
234212

235213
const [_noSearchFocusNode, setNoSearchFocusNode] = useState<AbstractTreeNode<V, T> | undefined>();
236-
const noSearchFocusNode = useMemo(() => {
237-
if (
238-
_noSearchFocusNode &&
239-
findNested(renderNodes as AbstractTreeNode<V, T>[], (node) => node.enabled && node.id === _noSearchFocusNode.id)
240-
) {
241-
return _noSearchFocusNode;
214+
const noSearchFocusNode = (() => {
215+
if (_noSearchFocusNode) {
216+
const node = nodesMap.get(_noSearchFocusNode.id);
217+
if (node && node.enabled) {
218+
return node;
219+
}
242220
}
243221

244-
if (isArray(select)) {
245-
if (select.length > 0) {
246-
return findNested(renderNodes as AbstractTreeNode<V, T>[], (node) => node.enabled && node.checked);
247-
}
248-
} else {
249-
if (!isNull(select)) {
250-
return findNested(renderNodes as AbstractTreeNode<V, T>[], (node) => node.enabled && node.checked);
251-
}
222+
if (hasSelected) {
223+
return findNested(renderNodes as AbstractTreeNode<V, T>[], (node) => node.enabled && node.checked);
252224
}
253-
}, [_noSearchFocusNode, renderNodes, select]);
225+
})();
254226

255227
const [_searchFocusOption, setSearchFocusOption] = useState<DSearchOption<V, T> | undefined>();
256-
const searchFocusOption = useMemo(() => {
228+
const searchFocusOption = (() => {
257229
if (_searchFocusOption && findNested(searchOptions, (o) => o[TREE_NODE_KEY].enabled && o.value === _searchFocusOption.value)) {
258230
return _searchFocusOption;
259231
}
260232

261233
if (hasSearch) {
262234
return findNested(searchOptions, (o) => o[TREE_NODE_KEY].enabled);
263235
}
264-
}, [_searchFocusOption, hasSearch, searchOptions]);
236+
})();
265237

266238
const handleClear = () => {
267239
onClear?.();
@@ -273,30 +245,12 @@ function Cascader<V extends DId, T extends DCascaderOption<V>>(
273245
}
274246
};
275247

276-
const [selectedNode, suffixNode, selectedLabel] = useMemo(() => {
248+
const [selectedNode, suffixNode, selectedLabel] = (() => {
277249
let selectedNode: React.ReactNode = null;
278250
let suffixNode: React.ReactNode = null;
279251
let selectedLabel: string | undefined;
280252
if (dMultiple) {
281-
const selectedNodes: MultipleTreeNode<V, T>[] = [];
282-
let length = (select as V[]).length;
283-
const reduceArr = (arr: MultipleTreeNode<V, T>[]) => {
284-
for (const item of arr) {
285-
if (length === 0) {
286-
break;
287-
}
288-
if (item.children) {
289-
reduceArr(item.children);
290-
} else {
291-
const index = (select as V[]).findIndex((val) => val === item.id);
292-
if (index !== -1) {
293-
selectedNodes[index] = item;
294-
length -= 1;
295-
}
296-
}
297-
}
298-
};
299-
reduceArr(renderNodes as MultipleTreeNode<V, T>[]);
253+
const selectedNodes = (_select as V[]).map((v) => nodesMap.get(v) as MultipleTreeNode<V, T>);
300254

301255
suffixNode = (
302256
<DDropdown
@@ -314,12 +268,12 @@ function Cascader<V extends DId, T extends DCascaderOption<V>>(
314268
})}
315269
dCloseOnClick={false}
316270
onOptionClick={(id, option) => {
317-
const checkeds = (option.node as MultipleTreeNode<V, T>).changeStatus('UNCHECKED', select as V[]);
318-
changeSelectByCache(checkeds);
271+
const checkeds = (option.node as MultipleTreeNode<V, T>).changeStatus('UNCHECKED', select as Set<V>);
272+
changeSelect(Array.from(checkeds.keys()));
319273
}}
320274
>
321275
<DTag className={`${dPrefix}cascader__multiple-count`} dSize={size}>
322-
{(select as V[]).length}
276+
{(select as Set<V>).size}
323277
</DTag>
324278
</DDropdown>
325279
);
@@ -332,24 +286,22 @@ function Cascader<V extends DId, T extends DCascaderOption<V>>(
332286
onClose={(e) => {
333287
e.stopPropagation();
334288

335-
const checkeds = (node as MultipleTreeNode<V, T>).changeStatus('UNCHECKED', select as V[]);
336-
changeSelectByCache(checkeds);
289+
const checkeds = (node as MultipleTreeNode<V, T>).changeStatus('UNCHECKED', select as Set<V>);
290+
changeSelect(Array.from(checkeds.keys()));
337291
}}
338292
>
339293
{dCustomSelected ? dCustomSelected(node.origin) : node.origin.label}
340294
</DTag>
341295
));
342296
} else {
343297
if (!isNull(select)) {
344-
const node = findNested(renderNodes as SingleTreeNode<V, T>[], (node) => node.id === (select as V));
345-
if (node) {
346-
selectedLabel = getText(node);
347-
selectedNode = dCustomSelected ? dCustomSelected(node.origin) : selectedLabel;
348-
}
298+
const node = nodesMap.get(select as V)!;
299+
selectedLabel = getText(node);
300+
selectedNode = dCustomSelected ? dCustomSelected(node.origin) : selectedLabel;
349301
}
350302
}
351303
return [selectedNode, suffixNode, selectedLabel];
352-
}, [changeSelectByCache, dCustomSelected, dMultiple, dPrefix, disabled, renderNodes, select, size]);
304+
})();
353305

354306
return (
355307
<DSelectbox
@@ -419,10 +371,10 @@ function Cascader<V extends DId, T extends DCascaderOption<V>>(
419371
{dLoading && (
420372
<div
421373
className={getClassName(`${dPrefix}cascader__loading`, {
422-
[`${dPrefix}cascader__loading--empty`]: dOptions.length === 0,
374+
[`${dPrefix}cascader__loading--empty`]: (hasSearch ? searchOptions : renderNodes).length === 0,
423375
})}
424376
>
425-
<LoadingOutlined dSize={dOptions.length === 0 ? 18 : 24} dSpin />
377+
<LoadingOutlined dSize={(hasSearch ? searchOptions : renderNodes).length === 0 ? 18 : 24} dSpin />
426378
</div>
427379
)}
428380
{hasSearch ? (
@@ -436,7 +388,7 @@ function Cascader<V extends DId, T extends DCascaderOption<V>>(
436388
dMultiple={dMultiple}
437389
dOnlyLeafSelectable={dOnlyLeafSelectable}
438390
dFocusVisible={focusVisible}
439-
onSelectedChange={changeSelectByCache}
391+
onSelectedChange={changeSelect}
440392
onClose={() => {
441393
changeVisible(false);
442394
}}
@@ -459,7 +411,7 @@ function Cascader<V extends DId, T extends DCascaderOption<V>>(
459411
dOnlyLeafSelectable={dOnlyLeafSelectable}
460412
dFocusVisible={focusVisible}
461413
dRoot
462-
onSelectedChange={changeSelectByCache}
414+
onSelectedChange={changeSelect}
463415
onClose={() => {
464416
changeVisible(false);
465417
}}

packages/ui/src/components/cascader/List.tsx

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import type { DId } from '../../utils/global';
22
import type { DVirtualScrollRef } from '../_virtual-scroll';
3-
import type { AbstractTreeNode, MultipleTreeNode, SingleTreeNode } from '../tree';
3+
import type { AbstractTreeNode, MultipleTreeNode } from '../tree';
44
import type { DCascaderOption } from './Cascader';
55
import type { Subject } from 'rxjs';
66

@@ -17,7 +17,7 @@ export interface DListProps<ID extends DId, T> {
1717
dListId?: string;
1818
dGetOptionId: (value: ID) => string;
1919
dNodes: AbstractTreeNode<ID, T>[];
20-
dSelected: ID | null | ID[];
20+
dSelected: ID | null | Set<ID>;
2121
dFocusNode: AbstractTreeNode<ID, T> | undefined;
2222
dCustomOption?: (option: T) => React.ReactNode;
2323
dMultiple: boolean;
@@ -78,11 +78,10 @@ export function DList<ID extends DId, T extends DCascaderOption<ID>>(props: DLis
7878

7979
const changeSelectByClick = useEventCallback((option: AbstractTreeNode<ID, T>) => {
8080
if (dMultiple) {
81-
const checkeds = (option as MultipleTreeNode<ID, T>).changeStatus(option.checked ? 'UNCHECKED' : 'CHECKED', dSelected as ID[]);
82-
onSelectedChange(checkeds);
81+
const checkeds = (option as MultipleTreeNode<ID, T>).changeStatus(option.checked ? 'UNCHECKED' : 'CHECKED', dSelected as Set<ID>);
82+
onSelectedChange(Array.from(checkeds.keys()));
8383
} else {
8484
if (!dOnlyLeafSelectable || option.isLeaf) {
85-
(option as SingleTreeNode<ID, T>).setChecked();
8685
onSelectedChange(option.id);
8786
}
8887
if (option.isLeaf) {

0 commit comments

Comments
 (0)