form 渲染性能优化

This commit is contained in:
liaoxuezhi 2019-11-14 17:13:44 +08:00
parent d93f311f6f
commit ecefaab794
8 changed files with 226 additions and 159 deletions

View File

@ -43,7 +43,10 @@ interface CheckboxProps {
}
export class Checkbox extends React.Component<CheckboxProps, any> {
static defaultProps = {
static defaultProps: Pick<
CheckboxProps,
'trueValue' | 'falseValue' | 'type'
> = {
trueValue: true,
falseValue: false,
type: 'checkbox'

View File

@ -6,7 +6,6 @@ import chunk = require('lodash/chunk');
export interface CheckboxesProps extends OptionsControlProps {
placeholder?: any;
disabled?: boolean;
itemClassName?: string;
columnsCount?: number;
labelClassName?: string;

View File

@ -107,6 +107,7 @@ export default class ComboControl extends React.Component<ComboProps> {
];
subForms: Array<any> = [];
subFormDefaultValues: Array<{index: number; values: any}> = [];
keys: Array<string> = [];
dragTip?: HTMLElement;
sortable?: Sortable;
@ -326,19 +327,31 @@ export default class ComboControl extends React.Component<ComboProps> {
return;
}
let value = this.getValueAsArray();
const newValue = flat ? values.flat : {...values};
this.subFormDefaultValues.push({
index,
values
});
if (!isObjectShallowModified(value[index], newValue)) {
if (this.subFormDefaultValues.length !== this.subForms.length) {
return;
}
value[index] = flat ? values.flat : {...values};
let value = this.getValueAsArray();
this.subFormDefaultValues.forEach(({index, values}) => {
const newValue = flat ? values.flat : {...values};
if (!isObjectShallowModified(value[index], newValue)) {
return;
}
value[index] = flat ? values.flat : {...values};
});
if (flat && joinValues) {
value = value.join(delimiter || ',');
}
this.props.onChange(value);
this.props.setPrinstineValue(value);
}
handleSingleFormInit(values: any) {

View File

@ -8,6 +8,7 @@ import {anyChanged, promisify, isObject, getVariable} from '../../utils/helper';
import {Schema} from '../../types';
import {IIRendererStore} from '../../store';
import {ScopedContext, IScopedContext} from '../../Scoped';
import {reaction} from 'mobx';
export interface ControlProps extends RendererProps {
control: {
@ -35,15 +36,25 @@ export interface ControlProps extends RendererProps {
removeHook: (fn: () => any) => void;
}
export default class FormControl extends React.Component<ControlProps> {
interface ControlState {
value: any;
}
export default class FormControl extends React.PureComponent<
ControlProps,
ControlState
> {
public model: IFormItemStore | undefined;
control: any;
hook?: () => any;
hook2?: () => any;
reaction?: () => void;
static defaultProps: Partial<ControlProps> = {};
static defaultProps = {};
lazyValidate: Function;
lazyEmitChange: (value: any, submitOnChange: boolean) => void;
state = {value: this.props.control.value};
componentWillMount() {
const {
formStore: form,
@ -76,12 +87,16 @@ export default class FormControl extends React.Component<ControlProps> {
trailing: true,
leading: false
});
this.lazyEmitChange = debouce(this.emitChange.bind(this), 250, {
trailing: true,
leading: false
});
if (!name) {
return;
}
this.model = form.registryItem(name, {
const model = (this.model = form.registryItem(name, {
id,
type,
required,
@ -95,7 +110,7 @@ export default class FormControl extends React.Component<ControlProps> {
labelField,
joinValues,
extractValue
});
}));
if (
this.model.unique &&
@ -105,6 +120,15 @@ export default class FormControl extends React.Component<ControlProps> {
const combo = form.parentStore as IComboStore;
combo.bindUniuqueItem(this.model);
}
// 同步 value
this.setState({
value: model.value
});
this.reaction = reaction(
() => model.value,
value => this.setState({value})
);
}
componentDidMount() {
@ -118,7 +142,7 @@ export default class FormControl extends React.Component<ControlProps> {
if (name && form !== store) {
const value = getVariable(store.data, name);
if (typeof value !== 'undefined' && value !== this.getValue()) {
this.handleChange(value);
this.emitChange(value, false);
}
}
@ -151,7 +175,7 @@ export default class FormControl extends React.Component<ControlProps> {
this.model && this.disposeModel();
// name 是后面才有的,比如编辑模式下就会出现。
this.model = form.registryItem(nextProps.control.name, {
const model = (this.model = form.registryItem(nextProps.control.name, {
id: nextProps.control.id,
type: nextProps.control.type,
required: nextProps.control.required,
@ -165,8 +189,16 @@ export default class FormControl extends React.Component<ControlProps> {
joinValues: nextProps.control.joinValues,
extractValue: nextProps.control.extractValue,
messages: nextProps.control.validationErrors
});
}));
// this.forceUpdate();
this.setState({
value: model.value
});
this.reaction && this.reaction();
this.reaction = reaction(
() => model.value,
value => this.setState({value})
);
}
if (
@ -227,7 +259,7 @@ export default class FormControl extends React.Component<ControlProps> {
(value = getVariable(data as any, name)) !==
getVariable(prevProps.data, name)
) {
this.handleChange(value);
this.emitChange(value, false);
}
}
@ -235,6 +267,9 @@ export default class FormControl extends React.Component<ControlProps> {
this.hook && this.props.removeHook(this.hook);
this.hook2 && this.props.removeHook(this.hook2);
this.disposeModel();
(this.lazyValidate as any).cancel();
(this.lazyEmitChange as any).cancel();
this.reaction && this.reaction();
}
disposeModel() {
@ -305,18 +340,12 @@ export default class FormControl extends React.Component<ControlProps> {
handleChange(
value: any,
submitOnChange: boolean = this.props.control.submitOnChange
submitOnChange: boolean = this.props.control.submitOnChange,
changeImmediately: boolean = false
) {
const {
formStore: form,
onChange,
control: {
validateOnChange,
name,
pipeOut,
onChange: onFormItemChange,
type
}
control: {type}
} = this.props;
// todo 以后想办法不要強耦合类型。
@ -324,6 +353,31 @@ export default class FormControl extends React.Component<ControlProps> {
onChange && onChange(...(arguments as any));
return;
}
this.setState(
{
value
},
() =>
changeImmediately
? this.emitChange(value, submitOnChange)
: this.lazyEmitChange(value, submitOnChange)
);
}
emitChange(
value: any,
submitOnChange: boolean = this.props.control.submitOnChange
) {
const {
formStore: form,
onChange,
control: {validateOnChange, name, pipeOut, onChange: onFormItemChange}
} = this.props;
if (!this.model) {
return;
}
const oldValue = this.model.value;
if (pipeOut) {
@ -411,11 +465,8 @@ export default class FormControl extends React.Component<ControlProps> {
}
getValue() {
const {control, formStore: form} = this.props;
const model = this.model;
// let value:any = model ? (typeof model.value === 'undefined' ? '' : model.value) : (control.value || '');
let value: any = model ? model.value : control.value;
const {formStore: form, control} = this.props;
let value: any = this.state.value;
if (control.pipeIn) {
value = control.pipeIn(value, form.data);

View File

@ -664,6 +664,63 @@ export class FormItemWrap extends React.Component<FormItemProps> {
}
}
// 白名单形式,只有这些属性发生变化,才会往下更新。
// 除非配置 strictMode
export const detectProps = [
// 'formPristine', // 理论来说,不需要,因为 formPristine 更新到时候 value 肯定也会更新。
'addable',
'addButtonClassName',
'addButtonText',
'addOn',
'btnClassName',
'btnLabel',
'btnDisabled',
'className',
'clearable',
'columns',
'columnsCount',
'controls',
'desc',
'description',
'disabled',
'draggable',
'editable',
'editButtonClassName',
'formHorizontal',
'formMode',
'hideRoot',
'horizontal',
'icon',
'inline',
'inputClassName',
'label',
'labelClassName',
'labelField',
'language',
'level',
'max',
'maxRows',
'min',
'minRows',
'multiLine',
'multiple',
'option',
'placeholder',
'removable',
'required',
'remark',
'hint',
'rows',
'searchable',
'showCompressOptions',
'size',
'step',
'showInput',
'unit',
'value',
'diffValue'
];
export function registerFormItem(config: FormItemConfig): RendererConfig {
let Control = config.component;
@ -700,7 +757,6 @@ export function registerFormItem(config: FormItemConfig): RendererConfig {
renderDescription: config.renderDescription,
sizeMutable: config.sizeMutable,
wrap: config.wrap,
strictMode: config.strictMode,
...Control.defaultProps
};
static propsList: any = [
@ -709,6 +765,7 @@ export function registerFormItem(config: FormItemConfig): RendererConfig {
'onChange',
'setPrinstineValue',
'readOnly',
'strictMode',
...((Control as any).propsList || [])
];
@ -736,71 +793,12 @@ export function registerFormItem(config: FormItemConfig): RendererConfig {
}
shouldComponentUpdate(nextProps: FormControlProps) {
if (nextProps.strictMode === false) {
if (nextProps.strictMode === false || config.strictMode === false) {
return true;
}
// 把可能会影响视图的白名单弄出来,减少重新渲染次数。
if (
anyChanged(
[
'formPristine',
'addable',
'addButtonClassName',
'addButtonText',
'addOn',
'btnClassName',
'btnLabel',
'btnDisabled',
'className',
'clearable',
'columns',
'columnsCount',
'controls',
'desc',
'description',
'disabled',
'draggable',
'editable',
'editButtonClassName',
'formHorizontal',
'formMode',
'hideRoot',
'horizontal',
'icon',
'inline',
'inputClassName',
'label',
'labelClassName',
'labelField',
'language',
'level',
'max',
'maxRows',
'min',
'minRows',
'multiLine',
'multiple',
'option',
'placeholder',
'removable',
'required',
'remark',
'hint',
'rows',
'searchable',
'showCompressOptions',
'size',
'step',
'showInput',
'unit',
'value',
'diffValue'
],
this.props,
nextProps
)
) {
if (anyChanged(detectProps, this.props, nextProps)) {
return true;
}

View File

@ -14,7 +14,12 @@ import {
getTree
} from '../../utils/helper';
import {reaction} from 'mobx';
import {FormControlProps, registerFormItem, FormItemBasicConfig} from './Item';
import {
FormControlProps,
registerFormItem,
FormItemBasicConfig,
detectProps as itemDetectProps
} from './Item';
import {IFormItemStore} from '../../store/formItem';
export type OptionsControlComponent = React.ComponentType<FormControlProps>;
@ -71,6 +76,21 @@ export interface OptionsProps extends FormControlProps, OptionProps {
optionLabel?: string;
}
export const detectProps = itemDetectProps.concat([
'options',
'size',
'buttons',
'columnsCount',
'multiple',
'hideRoot',
'checkAll',
'showIcon',
'showRadio',
'btnDisabled',
'joinValues',
'extractValue'
]);
export function registerOptionsControl(config: OptionsConfig) {
const Control = config.component;
@ -93,7 +113,7 @@ export function registerOptionsControl(config: OptionsConfig) {
: [];
static ComposedComponent = Control;
reaction: any;
reaction?: () => void;
input: any;
componentWillMount() {
@ -110,19 +130,15 @@ export function registerOptionsControl(config: OptionsConfig) {
addHook,
formInited,
valueField,
options
options,
value
} = this.props;
if (formItem) {
formItem.setOptions(normalizeOptions(options));
this.reaction = reaction(
() =>
JSON.stringify([
formItem.loading,
formItem.selectedOptions,
formItem.filteredOptions
]),
() => JSON.stringify([formItem.loading, formItem.filteredOptions]),
() => this.forceUpdate()
);
}
@ -140,12 +156,15 @@ export function registerOptionsControl(config: OptionsConfig) {
if (formItem && joinValues === false && defaultValue) {
const selectedOptions = extractValue
? formItem.selectedOptions.map(
(selectedOption: Option) => selectedOption[valueField || 'value']
)
: formItem.selectedOptions;
? formItem
.getSelectedOptions(value)
.map(
(selectedOption: Option) =>
selectedOption[valueField || 'value']
)
: formItem.getSelectedOptions(value);
setPrinstineValue(
multiple ? selectedOptions.concat() : formItem.selectedOptions[0]
multiple ? selectedOptions.concat() : selectedOptions[0]
);
}
@ -164,37 +183,7 @@ export function registerOptionsControl(config: OptionsConfig) {
return true;
}
if (
anyChanged(
[
'formPristine',
'addOn',
'disabled',
'placeholder',
'required',
'formMode',
'className',
'inputClassName',
'labelClassName',
'label',
'inline',
'options',
'size',
'btnClassName',
'btnActiveClassName',
'buttons',
'columnsCount',
'multiple',
'hideRoot',
'checkAll',
'showIcon',
'showRadio',
'btnDisabled'
],
this.props,
nextProps
)
) {
if (anyChanged(detectProps, this.props, nextProps)) {
return true;
}
@ -280,10 +269,9 @@ export function registerOptionsControl(config: OptionsConfig) {
extractValue === false &&
(typeof value === 'string' || typeof value === 'number')
) {
const selectedOptions = formItem.getSelectedOptions(value);
formItem.changeValue(
multiple
? formItem.selectedOptions.concat()
: formItem.selectedOptions[0]
multiple ? selectedOptions.concat() : selectedOptions[0]
);
} else if (
extractValue === true &&
@ -297,9 +285,11 @@ export function registerOptionsControl(config: OptionsConfig) {
typeof value === 'number'
)
) {
const selectedOptions = formItem.selectedOptions.map(
(selectedOption: Option) => selectedOption[valueField || 'value']
);
const selectedOptions = formItem
.getSelectedOptions(value)
.map(
(selectedOption: Option) => selectedOption[valueField || 'value']
);
formItem.changeValue(
multiple ? selectedOptions.concat() : selectedOptions[0]
);
@ -326,14 +316,15 @@ export function registerOptionsControl(config: OptionsConfig) {
clearable,
resetValue,
multiple,
formItem
formItem,
value
} = this.props;
if (!formItem) {
return;
}
let valueArray = formItem.selectedOptions.concat();
let valueArray = formItem.getSelectedOptions(value).concat();
const idx = valueArray.indexOf(option);
let newValue: string | Array<Option> | Option = '';
@ -375,6 +366,7 @@ export function registerOptionsControl(config: OptionsConfig) {
@autobind
handleToggleAll() {
const {
value,
onChange,
joinValues,
extractValue,
@ -389,8 +381,9 @@ export function registerOptionsControl(config: OptionsConfig) {
return;
}
const selectedOptions = formItem.getSelectedOptions(value);
let valueArray =
formItem.selectedOptions.length === formItem.filteredOptions.length
selectedOptions.length === formItem.filteredOptions.length
? []
: formItem.filteredOptions.concat();

View File

@ -187,9 +187,14 @@ export const FormStore = ServiceStore.named('FormStore')
self.updateData(data);
}
function syncOptions() {
self.items.forEach(item => item.syncOptions());
}
const syncOptions = debounce(
() => self.items.forEach(item => item.syncOptions()),
250,
{
trailing: true,
leading: false
}
);
const saveRemote: (
api: Api,
@ -448,12 +453,18 @@ export const FormStore = ServiceStore.named('FormStore')
self.inited = value;
}
const setPersistData = debounce(() => {
localStorage.setItem(
location.pathname + self.path,
JSON.stringify(self.data)
);
}, 250);
const setPersistData = debounce(
() =>
localStorage.setItem(
location.pathname + self.path,
JSON.stringify(self.data)
),
250,
{
trailing: true,
leading: false
}
);
function getPersistData() {
self.persistData = true;

View File

@ -11,6 +11,7 @@ import {IRendererStore} from '.';
import {normalizeOptions} from '../components/Select';
import find = require('lodash/find');
import {SimpleMap} from '../utils/SimpleMap';
import memoize = require('lodash/memoize');
interface IOption {
value?: string | number | null;
@ -105,10 +106,8 @@ export const FormItemStore = types
return getLastOptionValue();
},
getSelectedOptions(value: any = getValue()) {
if (value === getValue()) {
return self.selectedOptions;
} else if (typeof value === 'undefined') {
getSelectedOptions: memoize((value: any = getValue()) => {
if (typeof value === 'undefined') {
return [];
}
@ -165,7 +164,7 @@ export const FormItemStore = types
});
return selectedOptions;
}
})
};
})