From f6847b13c3072528e6ced083fa067b7d5bd58582 Mon Sep 17 00:00:00 2001 From: Kermit Date: Fri, 5 Jun 2020 01:19:35 +0800 Subject: [PATCH] chore: optimize multiple onChange performence(#22822) --- src/generate.tsx | 40 ++++++++++++++++++++++-------------- src/hooks/useCacheOptions.ts | 32 +++++++++++++++++++++++++++++ src/interface/generator.ts | 32 +++++++++++------------------ src/utils/commonUtil.ts | 24 ++++++++-------------- src/utils/valueUtil.ts | 25 ++++++++-------------- 5 files changed, 87 insertions(+), 66 deletions(-) create mode 100644 src/hooks/useCacheOptions.ts diff --git a/src/generate.tsx b/src/generate.tsx index 490278906..b897bee46 100644 --- a/src/generate.tsx +++ b/src/generate.tsx @@ -30,6 +30,7 @@ import { INTERNAL_PROPS_MARK, SelectSource, CustomTagProps, + FlattenOptionMapType, } from './interface/generator'; import { OptionListProps, RefOptionListProps } from './OptionList'; import { toInnerValue, toOuterValues, removeLastEnabledValue, getUUID } from './utils/commonUtil'; @@ -40,6 +41,7 @@ import useLayoutEffect from './hooks/useLayoutEffect'; import { getSeparatedContent } from './utils/valueUtil'; import useSelectTriggerControl from './hooks/useSelectTriggerControl'; import useCacheDisplayValue from './hooks/useCacheDisplayValue'; +import useCacheOptions from './hooks/useCacheOptions'; const DEFAULT_OMIT_PROPS = [ 'removeIcon', @@ -182,14 +184,17 @@ export interface GenerateConfig { /** Flatten nest options into raw option list */ flattenOptions: (options: OptionsType, props: any) => FlattenOptionsType; /** Convert single raw value into { label, value } format. Will be called by each value */ - getLabeledValue: GetLabeledValue>; + getLabeledValue: GetLabeledValue>; filterOptions: FilterOptions; findValueOption: ( values: RawValueType[], - options: FlattenOptionsType, + optionMap: FlattenOptionMapType, ) => OptionsType; /** Check if a value is disabled */ - isValueDisabled: (value: RawValueType, options: FlattenOptionsType) => boolean; + isValueDisabled: ( + value: RawValueType, + optionMap: FlattenOptionMapType, + ) => boolean; warningProps?: (props: any) => void; fillOptionsWithMissingValue?: ( options: OptionsType, @@ -419,6 +424,8 @@ export default function generateSelector< [mergedOptions], ); + const optionMap = useCacheOptions(mergedRawValue, mergedFlattenOptions); + // Display options for OptionList const displayOptions = React.useMemo(() => { if (!mergedSearchValue || !mergedShowSearch) { @@ -455,7 +462,7 @@ export default function generateSelector< () => mergedRawValue.map((val: RawValueType) => { const displayValue = getLabeledValue(val, { - options: mergedFlattenOptions, + optionMap, prevValue: baseValue, labelInValue: mergedLabelInValue, optionLabelProp: mergedOptionLabelProp, @@ -463,7 +470,7 @@ export default function generateSelector< return { ...displayValue, - disabled: isValueDisabled(val, mergedFlattenOptions), + disabled: isValueDisabled(val, optionMap), }; }), [baseValue, mergedOptions], @@ -473,13 +480,13 @@ export default function generateSelector< displayValues = useCacheDisplayValue(displayValues); const triggerSelect = (newValue: RawValueType, isSelect: boolean, source: SelectSource) => { - const outOption = findValueOption([newValue], mergedFlattenOptions)[0]; + const outOption = findValueOption([newValue], optionMap)[0]; if (!internalProps.skipTriggerSelect) { // Skip trigger `onSelect` or `onDeselect` if configured const selectValue = (mergedLabelInValue ? getLabeledValue(newValue, { - options: mergedFlattenOptions, + optionMap, prevValue: baseValue, labelInValue: mergedLabelInValue, optionLabelProp: mergedOptionLabelProp, @@ -508,18 +515,21 @@ export default function generateSelector< return; } - const outValues = toOuterValues>(Array.from(newRawValues), { - labelInValue: mergedLabelInValue, - options: mergedFlattenOptions, - getLabeledValue, - prevValue: baseValue, - optionLabelProp: mergedOptionLabelProp, - }); + const outValues = toOuterValues>( + Array.from(newRawValues), + { + labelInValue: mergedLabelInValue, + optionMap, + getLabeledValue, + prevValue: baseValue, + optionLabelProp: mergedOptionLabelProp, + }, + ); const outValue: ValueType = (isMultiple ? outValues : outValues[0]) as ValueType; // Skip trigger if prev & current value is both empty if (onChange && (mergedRawValue.length !== 0 || outValues.length !== 0)) { - const outOptions = findValueOption(newRawValues, mergedFlattenOptions); + const outOptions = findValueOption(newRawValues, optionMap); onChange(outValue, isMultiple ? outOptions : outOptions[0]); } diff --git a/src/hooks/useCacheOptions.ts b/src/hooks/useCacheOptions.ts new file mode 100644 index 000000000..4cd4fc35e --- /dev/null +++ b/src/hooks/useCacheOptions.ts @@ -0,0 +1,32 @@ +import * as React from 'react'; +import { RawValueType, FlattenOptionsType, Key } from '../interface/generator'; + +export default function useCacheOptions< + OptionsType extends { + value?: RawValueType; + label?: React.ReactNode; + key?: Key; + disabled?: boolean; + }[] +>( + values: RawValueType[], + options: FlattenOptionsType, +): Map { + const prevOptionMapRef = React.useRef>(null); + + const optionMap = React.useMemo(() => { + const map: Map = new Map(); + options.forEach(item => { + if (!item.group) { + const { data } = item; + // Check if match + map.set(data.value, data); + } + }); + return map; + }, [values, options]); + + prevOptionMapRef.current = optionMap; + + return optionMap; +} diff --git a/src/interface/generator.ts b/src/interface/generator.ts index f56dad888..a6b789f25 100644 --- a/src/interface/generator.ts +++ b/src/interface/generator.ts @@ -14,19 +14,13 @@ export interface LabelValueType { value?: RawValueType; label?: React.ReactNode; } -export type DefaultValueType = - | RawValueType - | RawValueType[] - | LabelValueType - | LabelValueType[]; +export type DefaultValueType = RawValueType | RawValueType[] | LabelValueType | LabelValueType[]; export interface DisplayLabelValueType extends LabelValueType { disabled?: boolean; } -export type SingleType = MixType extends (infer Single)[] - ? Single - : MixType; +export type SingleType = MixType extends (infer Single)[] ? Single : MixType; export type OnClear = () => void; @@ -39,10 +33,10 @@ export type CustomTagProps = { }; // ==================================== Generator ==================================== -export type GetLabeledValue = ( +export type GetLabeledValue = ( value: RawValueType, config: { - options: FOT; + optionMap: FOT; prevValue: DefaultValueType; labelInValue: boolean; optionLabelProp: string; @@ -59,19 +53,12 @@ export type FilterOptions = ( }, ) => OptionsType; -export type FilterFunc = ( - inputValue: string, - option?: OptionType, -) => boolean; +export type FilterFunc = (inputValue: string, option?: OptionType) => boolean; export declare function RefSelectFunc( - Component: React.RefForwardingComponent< - RefSelectProps, - SelectProps - >, + Component: React.RefForwardingComponent>, ): React.ForwardRefExoticComponent< - React.PropsWithoutRef> & - React.RefAttributes + React.PropsWithoutRef> & React.RefAttributes >; export type FlattenOptionsType = { @@ -80,3 +67,8 @@ export type FlattenOptionsType = { /** Used for customize data */ [name: string]: any; // eslint-disable-line @typescript-eslint/no-explicit-any }[]; + +export type FlattenOptionMapType = Map< + DefaultValueType, + OptionType +>; diff --git a/src/utils/commonUtil.ts b/src/utils/commonUtil.ts index a4a2d2ee3..42d1038e8 100644 --- a/src/utils/commonUtil.ts +++ b/src/utils/commonUtil.ts @@ -3,7 +3,7 @@ import { GetLabeledValue, LabelValueType, DefaultValueType, - FlattenOptionsType, + FlattenOptionMapType, } from '../interface/generator'; export function toArray(value: T | T[]): T[] { @@ -27,8 +27,8 @@ export function toInnerValue( const values = Array.isArray(value) ? value : [value]; if (labelInValue) { - return (values as LabelValueType[]).map( - ({ key, value: val }: LabelValueType) => (val !== undefined ? val : key), + return (values as LabelValueType[]).map(({ key, value: val }: LabelValueType) => + (val !== undefined ? val : key), ); } @@ -38,19 +38,19 @@ export function toInnerValue( /** * Convert internal value into out event value */ -export function toOuterValues( +export function toOuterValues( valueList: RawValueType[], { optionLabelProp, labelInValue, prevValue, - options, + optionMap, getLabeledValue, }: { optionLabelProp: string; labelInValue: boolean; getLabeledValue: GetLabeledValue; - options: FOT; + optionMap: FOT; prevValue: DefaultValueType; }, ): RawValueType[] | LabelValueType[] { @@ -59,7 +59,7 @@ export function toOuterValues( if (labelInValue) { values = values.map(val => getLabeledValue(val, { - options, + optionMap, prevValue, labelInValue, optionLabelProp, @@ -77,11 +77,7 @@ export function removeLastEnabledValue< const newValues = [...values]; let removeIndex: number; - for ( - removeIndex = measureValues.length - 1; - removeIndex >= 0; - removeIndex -= 1 - ) { + for (removeIndex = measureValues.length - 1; removeIndex >= 0; removeIndex -= 1) { if (!measureValues[removeIndex].disabled) { break; } @@ -101,9 +97,7 @@ export function removeLastEnabledValue< } export const isClient = - typeof window !== 'undefined' && - window.document && - window.document.documentElement; + typeof window !== 'undefined' && window.document && window.document.documentElement; /** Is client side and not jsdom */ export const isBrowserClient = process.env.NODE_ENV !== 'test' && isClient; diff --git a/src/utils/valueUtil.ts b/src/utils/valueUtil.ts index 5a129e09d..b6c583fca 100644 --- a/src/utils/valueUtil.ts +++ b/src/utils/valueUtil.ts @@ -89,26 +89,16 @@ function injectPropsWithOption(option: T): T { export function findValueOption( values: RawValueType[], - options: FlattenOptionData[], + optionMap: Map, ): OptionData[] { - const optionMap: Map = new Map(); - - options.forEach(flattenItem => { - if (!flattenItem.group) { - const data = flattenItem.data as OptionData; - // Check if match - optionMap.set(data.value, data); - } - }); - return values.map(val => injectPropsWithOption(optionMap.get(val))); } -export const getLabeledValue: GetLabeledValue = ( +export const getLabeledValue: GetLabeledValue> = ( value, - { options, prevValue, labelInValue, optionLabelProp }, + { optionMap, prevValue, labelInValue, optionLabelProp }, ) => { - const item = findValueOption([value], options)[0]; + const item = findValueOption([value], optionMap)[0]; const result: LabelValueType = { value, }; @@ -245,8 +235,11 @@ export function getSeparatedContent(text: string, tokens: string[]): string[] { return match ? list : null; } -export function isValueDisabled(value: RawValueType, options: FlattenOptionData[]): boolean { - const option = findValueOption([value], options)[0]; +export function isValueDisabled( + value: RawValueType, + optionMap: Map, +): boolean { + const option = findValueOption([value], optionMap)[0]; return option.disabled; }