Skip to content

Commit

Permalink
perf(ui): optimizing handle of big data
Browse files Browse the repository at this point in the history
  • Loading branch information
xiejay97 committed Jul 26, 2022
1 parent ae97c22 commit 542f220
Show file tree
Hide file tree
Showing 19 changed files with 251 additions and 405 deletions.
2 changes: 1 addition & 1 deletion packages/site/src/app/components/layout/header/Header.scss
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@
min-width: 36px;
height: 36px;
margin: 0;
font-family: inherit;
font: inherit;
color: var(--d-text-color);
text-align: unset;
text-transform: none;
Expand Down
8 changes: 4 additions & 4 deletions packages/ui/src/components/auto-complete/AutoComplete.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import type { DFocusVisibleRenderProps } from '../_focus-visible';
import type { DVirtualScrollRef } from '../_virtual-scroll';

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

import {
Expand Down Expand Up @@ -125,13 +125,13 @@ function AutoComplete<T extends DAutoCompleteOption>(
useUpdatePosition(updatePosition, visible);

const [_focusOption, setFocusOption] = useState<DNestedChildren<T> | undefined>();
const focusOption = useMemo(() => {
const focusOption = (() => {
if (_focusOption && findNested(dOptions, (o) => canSelectOption(o) && o.value === _focusOption.value)) {
return _focusOption;
}

return findNested(dOptions, (o) => canSelectOption(o));
}, [_focusOption, canSelectOption, dOptions]);
})();

const changeFocusOption = (option?: DNestedChildren<T>) => {
if (!isUndefined(option)) {
Expand Down Expand Up @@ -371,7 +371,7 @@ function AutoComplete<T extends DAutoCompleteOption>(
style={{ paddingLeft: parent.length === 0 ? undefined : 12 + 8 }}
title={optionValue}
role="option"
aria-auto-completeed={false}
aria-selected={false}
aria-disabled={optionDisabled}
onClick={() => {
if (!optionDisabled) {
Expand Down
192 changes: 72 additions & 120 deletions packages/ui/src/components/cascader/Cascader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ import type { DFormControl } from '../form';
import type { DSelectOption } from '../select';
import type { AbstractTreeNode } from '../tree';

import { isArray, isNull } from 'lodash';
import React, { useCallback, useMemo, useState, useId, useRef } from 'react';
import { isNull } from 'lodash';
import React, { useCallback, useState, useId, useMemo } from 'react';

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

const renderNodes = useMemo(
() =>
dOptions.map((option) =>
dMultiple
? new MultipleTreeNode(option, (o) => o.value, {
disabled: option.disabled,
})
: new SingleTreeNode(option, (o) => o.value, {
disabled: option.disabled,
})
),
[dMultiple, dOptions]
);
const nodesMap = useMemo(() => {
const nodes = new Map<V, AbstractTreeNode<V, T>>();
const reduceArr = (arr: AbstractTreeNode<V, T>[]) => {
for (const item of arr) {
nodes.set(item.id, item);
if (item.children) {
reduceArr(item.children);
}
}
};
reduceArr(renderNodes);
return nodes;
}, [renderNodes]);

const [searchValue, setSearchValue] = useState('');

const [visible, changeVisible] = useDValue<boolean>(false, dVisible, onVisibleChange);
const formControlInject = useFormControl(dFormControl);
const [select, changeSelect] = useDValue<V | null | V[]>(
const [_select, changeSelect] = useDValue<V | null | V[]>(
dMultiple ? [] : null,
dModel,
(value) => {
if (onModelChange) {
if (isArray(value)) {
let length = value.length;
const options: DNestedChildren<T>[] = [];
const reduceArr = (arr: DNestedChildren<T>[]) => {
for (const item of arr) {
if (length === 0) {
break;
}
if (item.children) {
reduceArr(item.children);
} else {
const index = value.findIndex((val) => val === item.value);
if (index !== -1) {
options[index] = item;
length -= 1;
}
}
}
};
reduceArr(dOptions);

onModelChange(value, options);
if (dMultiple) {
onModelChange(
value,
(value as V[]).map((v) => nodesMap.get(v)?.origin)
);
} else {
if (isNull(value)) {
onModelChange(value, null);
} else {
onModelChange(
value,
findNested(dOptions, (option) => option.value === value)
);
}
onModelChange(value, isNull(value) ? null : nodesMap.get(value as V)?.origin);
}
}
},
undefined,
formControlInject
);
const select = useMemo(() => (dMultiple ? new Set(_select as V[]) : (_select as V | null)), [_select, dMultiple]);
renderNodes.forEach((node) => {
node.updateStatus(select);
});

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

const hasSearch = searchValue.length > 0;
const hasSelected = dMultiple ? (select as V[]).length > 0 : !isNull(select);

const checkedRef = useRef<SingleTreeNode<V, T>>();
const getRenderNodes = useCallback(
(select: V | null | V[]) => {
let renderNodes: SingleTreeNode<V, T>[] | MultipleTreeNode<V, T>[] = [];
if (dMultiple) {
renderNodes = dOptions.map(
(option) =>
new MultipleTreeNode(option, (o) => o.value, {
checkeds: select as V[],
disabled: option.disabled,
})
);
} else {
renderNodes = dOptions.map(
(option) =>
new SingleTreeNode(option, (o) => o.value, {
checkedRef,
checked: select as V | null,
disabled: option.disabled,
})
);
}
return renderNodes;
},
[dMultiple, dOptions]
);
const [renderNodes, changeSelectByCache] = useTreeData(select, getRenderNodes, changeSelect);
const hasSelected = dMultiple ? (select as Set<V>).size > 0 : !isNull(select);

const [focusVisible, setFocusVisible] = useState(false);

const filterFn = useCallback(
(option: T, searchStr = searchValue) => {
Expand All @@ -199,7 +179,7 @@ function Cascader<V extends DId, T extends DCascaderOption<V>>(
[dCustomSearch, searchValue]
);
const sortFn = dCustomSearch?.sort;
const searchOptions = useMemo(() => {
const searchOptions = (() => {
if (!hasSearch) {
return [];
}
Expand Down Expand Up @@ -228,40 +208,32 @@ function Cascader<V extends DId, T extends DCascaderOption<V>>(
searchOptions.sort((a, b) => sortFn(a[TREE_NODE_KEY].origin, b[TREE_NODE_KEY].origin));
}
return searchOptions;
}, [dMultiple, dOnlyLeafSelectable, filterFn, hasSearch, renderNodes, sortFn]);

const [focusVisible, setFocusVisible] = useState(false);
})();

const [_noSearchFocusNode, setNoSearchFocusNode] = useState<AbstractTreeNode<V, T> | undefined>();
const noSearchFocusNode = useMemo(() => {
if (
_noSearchFocusNode &&
findNested(renderNodes as AbstractTreeNode<V, T>[], (node) => node.enabled && node.id === _noSearchFocusNode.id)
) {
return _noSearchFocusNode;
const noSearchFocusNode = (() => {
if (_noSearchFocusNode) {
const node = nodesMap.get(_noSearchFocusNode.id);
if (node && node.enabled) {
return node;
}
}

if (isArray(select)) {
if (select.length > 0) {
return findNested(renderNodes as AbstractTreeNode<V, T>[], (node) => node.enabled && node.checked);
}
} else {
if (!isNull(select)) {
return findNested(renderNodes as AbstractTreeNode<V, T>[], (node) => node.enabled && node.checked);
}
if (hasSelected) {
return findNested(renderNodes as AbstractTreeNode<V, T>[], (node) => node.enabled && node.checked);
}
}, [_noSearchFocusNode, renderNodes, select]);
})();

const [_searchFocusOption, setSearchFocusOption] = useState<DSearchOption<V, T> | undefined>();
const searchFocusOption = useMemo(() => {
const searchFocusOption = (() => {
if (_searchFocusOption && findNested(searchOptions, (o) => o[TREE_NODE_KEY].enabled && o.value === _searchFocusOption.value)) {
return _searchFocusOption;
}

if (hasSearch) {
return findNested(searchOptions, (o) => o[TREE_NODE_KEY].enabled);
}
}, [_searchFocusOption, hasSearch, searchOptions]);
})();

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

const [selectedNode, suffixNode, selectedLabel] = useMemo(() => {
const [selectedNode, suffixNode, selectedLabel] = (() => {
let selectedNode: React.ReactNode = null;
let suffixNode: React.ReactNode = null;
let selectedLabel: string | undefined;
if (dMultiple) {
const selectedNodes: MultipleTreeNode<V, T>[] = [];
let length = (select as V[]).length;
const reduceArr = (arr: MultipleTreeNode<V, T>[]) => {
for (const item of arr) {
if (length === 0) {
break;
}
if (item.children) {
reduceArr(item.children);
} else {
const index = (select as V[]).findIndex((val) => val === item.id);
if (index !== -1) {
selectedNodes[index] = item;
length -= 1;
}
}
}
};
reduceArr(renderNodes as MultipleTreeNode<V, T>[]);
const selectedNodes = (_select as V[]).map((v) => nodesMap.get(v) as MultipleTreeNode<V, T>);

suffixNode = (
<DDropdown
Expand All @@ -314,12 +268,12 @@ function Cascader<V extends DId, T extends DCascaderOption<V>>(
})}
dCloseOnClick={false}
onOptionClick={(id, option) => {
const checkeds = (option.node as MultipleTreeNode<V, T>).changeStatus('UNCHECKED', select as V[]);
changeSelectByCache(checkeds);
const checkeds = (option.node as MultipleTreeNode<V, T>).changeStatus('UNCHECKED', select as Set<V>);
changeSelect(Array.from(checkeds.keys()));
}}
>
<DTag className={`${dPrefix}cascader__multiple-count`} dSize={size}>
{(select as V[]).length}
{(select as Set<V>).size}
</DTag>
</DDropdown>
);
Expand All @@ -332,24 +286,22 @@ function Cascader<V extends DId, T extends DCascaderOption<V>>(
onClose={(e) => {
e.stopPropagation();

const checkeds = (node as MultipleTreeNode<V, T>).changeStatus('UNCHECKED', select as V[]);
changeSelectByCache(checkeds);
const checkeds = (node as MultipleTreeNode<V, T>).changeStatus('UNCHECKED', select as Set<V>);
changeSelect(Array.from(checkeds.keys()));
}}
>
{dCustomSelected ? dCustomSelected(node.origin) : node.origin.label}
</DTag>
));
} else {
if (!isNull(select)) {
const node = findNested(renderNodes as SingleTreeNode<V, T>[], (node) => node.id === (select as V));
if (node) {
selectedLabel = getText(node);
selectedNode = dCustomSelected ? dCustomSelected(node.origin) : selectedLabel;
}
const node = nodesMap.get(select as V)!;
selectedLabel = getText(node);
selectedNode = dCustomSelected ? dCustomSelected(node.origin) : selectedLabel;
}
}
return [selectedNode, suffixNode, selectedLabel];
}, [changeSelectByCache, dCustomSelected, dMultiple, dPrefix, disabled, renderNodes, select, size]);
})();

return (
<DSelectbox
Expand Down Expand Up @@ -419,10 +371,10 @@ function Cascader<V extends DId, T extends DCascaderOption<V>>(
{dLoading && (
<div
className={getClassName(`${dPrefix}cascader__loading`, {
[`${dPrefix}cascader__loading--empty`]: dOptions.length === 0,
[`${dPrefix}cascader__loading--empty`]: (hasSearch ? searchOptions : renderNodes).length === 0,
})}
>
<LoadingOutlined dSize={dOptions.length === 0 ? 18 : 24} dSpin />
<LoadingOutlined dSize={(hasSearch ? searchOptions : renderNodes).length === 0 ? 18 : 24} dSpin />
</div>
)}
{hasSearch ? (
Expand All @@ -436,7 +388,7 @@ function Cascader<V extends DId, T extends DCascaderOption<V>>(
dMultiple={dMultiple}
dOnlyLeafSelectable={dOnlyLeafSelectable}
dFocusVisible={focusVisible}
onSelectedChange={changeSelectByCache}
onSelectedChange={changeSelect}
onClose={() => {
changeVisible(false);
}}
Expand All @@ -459,7 +411,7 @@ function Cascader<V extends DId, T extends DCascaderOption<V>>(
dOnlyLeafSelectable={dOnlyLeafSelectable}
dFocusVisible={focusVisible}
dRoot
onSelectedChange={changeSelectByCache}
onSelectedChange={changeSelect}
onClose={() => {
changeVisible(false);
}}
Expand Down
9 changes: 4 additions & 5 deletions packages/ui/src/components/cascader/List.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { DId } from '../../utils/global';
import type { DVirtualScrollRef } from '../_virtual-scroll';
import type { AbstractTreeNode, MultipleTreeNode, SingleTreeNode } from '../tree';
import type { AbstractTreeNode, MultipleTreeNode } from '../tree';
import type { DCascaderOption } from './Cascader';
import type { Subject } from 'rxjs';

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

const changeSelectByClick = useEventCallback((option: AbstractTreeNode<ID, T>) => {
if (dMultiple) {
const checkeds = (option as MultipleTreeNode<ID, T>).changeStatus(option.checked ? 'UNCHECKED' : 'CHECKED', dSelected as ID[]);
onSelectedChange(checkeds);
const checkeds = (option as MultipleTreeNode<ID, T>).changeStatus(option.checked ? 'UNCHECKED' : 'CHECKED', dSelected as Set<ID>);
onSelectedChange(Array.from(checkeds.keys()));
} else {
if (!dOnlyLeafSelectable || option.isLeaf) {
(option as SingleTreeNode<ID, T>).setChecked();
onSelectedChange(option.id);
}
if (option.isLeaf) {
Expand Down
Loading

0 comments on commit 542f220

Please # to comment.