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,7 +327,17 @@ export default class ComboControl extends React.Component<ComboProps> {
return;
}
this.subFormDefaultValues.push({
index,
values
});
if (this.subFormDefaultValues.length !== this.subForms.length) {
return;
}
let value = this.getValueAsArray();
this.subFormDefaultValues.forEach(({index, values}) => {
const newValue = flat ? values.flat : {...values};
if (!isObjectShallowModified(value[index], newValue)) {
@ -334,11 +345,13 @@ export default class ComboControl extends React.Component<ComboProps> {
}
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,87 +664,10 @@ export class FormItemWrap extends React.Component<FormItemProps> {
}
}
export function registerFormItem(config: FormItemConfig): RendererConfig {
let Control = config.component;
// 兼容老的 FormItem 用法。
if (config.validate && !Control.prototype.validate) {
const fn = config.validate;
Control.prototype.validate = function() {
// console.warn('推荐直接在类中定义,而不是 FormItem HOC 的参数中传入。');
const host = {
input: this
};
return fn.apply(host, arguments);
};
} else if (config.validate) {
console.error(
'FormItem配置中的 validate 将不起作用,因为类的成员函数中已经定义了 validate 方法,将优先使用类里面的实现。'
);
}
if (config.storeType) {
Control = HocStoreFactory({
storeType: config.storeType,
extendsData: config.extendsData
})(observer(Control));
delete config.storeType;
}
// @observer
class FormItemRenderer extends FormItemWrap {
static defaultProps = {
className: '',
renderLabel: config.renderLabel,
renderDescription: config.renderDescription,
sizeMutable: config.sizeMutable,
wrap: config.wrap,
strictMode: config.strictMode,
...Control.defaultProps
};
static propsList: any = [
'value',
'defaultValue',
'onChange',
'setPrinstineValue',
'readOnly',
...((Control as any).propsList || [])
];
static displayName = `FormItem${config.type ? `(${config.type})` : ''}`;
static ComposedComponent = Control;
ref: any;
constructor(props: FormItemProps) {
super(props);
this.refFn = this.refFn.bind(this);
}
componentWillMount() {
const {validations, formItem: model} = this.props;
// 组件注册的时候可能默认指定验证器类型
if (model && !validations && config.validations) {
model.config({
rules: config.validations
});
}
super.componentWillMount();
}
shouldComponentUpdate(nextProps: FormControlProps) {
if (nextProps.strictMode === false) {
return true;
}
// 把可能会影响视图的白名单弄出来,减少重新渲染次数。
if (
anyChanged(
[
'formPristine',
// 白名单形式,只有这些属性发生变化,才会往下更新。
// 除非配置 strictMode
export const detectProps = [
// 'formPristine', // 理论来说,不需要,因为 formPristine 更新到时候 value 肯定也会更新。
'addable',
'addButtonClassName',
'addButtonText',
@ -796,11 +719,86 @@ export function registerFormItem(config: FormItemConfig): RendererConfig {
'unit',
'value',
'diffValue'
],
this.props,
nextProps
)
) {
];
export function registerFormItem(config: FormItemConfig): RendererConfig {
let Control = config.component;
// 兼容老的 FormItem 用法。
if (config.validate && !Control.prototype.validate) {
const fn = config.validate;
Control.prototype.validate = function() {
// console.warn('推荐直接在类中定义,而不是 FormItem HOC 的参数中传入。');
const host = {
input: this
};
return fn.apply(host, arguments);
};
} else if (config.validate) {
console.error(
'FormItem配置中的 validate 将不起作用,因为类的成员函数中已经定义了 validate 方法,将优先使用类里面的实现。'
);
}
if (config.storeType) {
Control = HocStoreFactory({
storeType: config.storeType,
extendsData: config.extendsData
})(observer(Control));
delete config.storeType;
}
// @observer
class FormItemRenderer extends FormItemWrap {
static defaultProps = {
className: '',
renderLabel: config.renderLabel,
renderDescription: config.renderDescription,
sizeMutable: config.sizeMutable,
wrap: config.wrap,
...Control.defaultProps
};
static propsList: any = [
'value',
'defaultValue',
'onChange',
'setPrinstineValue',
'readOnly',
'strictMode',
...((Control as any).propsList || [])
];
static displayName = `FormItem${config.type ? `(${config.type})` : ''}`;
static ComposedComponent = Control;
ref: any;
constructor(props: FormItemProps) {
super(props);
this.refFn = this.refFn.bind(this);
}
componentWillMount() {
const {validations, formItem: model} = this.props;
// 组件注册的时候可能默认指定验证器类型
if (model && !validations && config.validations) {
model.config({
rules: config.validations
});
}
super.componentWillMount();
}
shouldComponentUpdate(nextProps: FormControlProps) {
if (nextProps.strictMode === false || config.strictMode === false) {
return true;
}
// 把可能会影响视图的白名单弄出来,减少重新渲染次数。
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
.getSelectedOptions(value)
.map(
(selectedOption: Option) =>
selectedOption[valueField || 'value']
)
: formItem.selectedOptions;
: 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,7 +285,9 @@ export function registerOptionsControl(config: OptionsConfig) {
typeof value === 'number'
)
) {
const selectedOptions = formItem.selectedOptions.map(
const selectedOptions = formItem
.getSelectedOptions(value)
.map(
(selectedOption: Option) => selectedOption[valueField || 'value']
);
formItem.changeValue(
@ -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(() => {
const setPersistData = debounce(
() =>
localStorage.setItem(
location.pathname + self.path,
JSON.stringify(self.data)
),
250,
{
trailing: true,
leading: false
}
);
}, 250);
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;
}
})
};
})