列表编辑性能优化
公司现有系统有一个字段管理功能,该功能编辑表的字段,数据结构是列表。每一条数据大概有15项编辑框,进入编辑模式后,所有的表单项都渲染,产生严重的性能问题。
组件采用antd 的Form Form.List,内里包裹Table组件,最初改造成,每次只让一条数据进入编辑状态,性能问题立马解决。但是领导不同意。
于是不得已,想着自己封装一个Form替换antd的Form。整个form都用原生组件,表单赋值用原生dom直接写value。也能解决性能问题。但是无法完美实现antd的Form.List的动态构建功能。
当form.list的表单项更新后,dom会自动创建。但是我自己实现的,采用useState自己管理状态,来触发更新,由于状态链条太深,引发react状态管理混乱,页面其他部分更新状态的同时,更新字段管理模块,页面状态不正确。
所以我还原了代码,开始从改造Table组件开始,重写Table组件的过程中,发现,性能问题的根本原因是Form.Item组件包裹,就是让表单项变成受控模式。只要把Form.Item去掉,性能立马从一次渲染(100条数据)1000ms变成200ms。
于是改造目标改成,去掉Form.Item,让所有表单项都不受控,自己写了一个CustomFormItem来包裹表单项,用来给表单初始时赋值(因为直接给value就变成受控模式,故包裹,当初次载入后直接原生dom赋值)。
用自己的form包装Form的formInstance,获取原生的form对象,当操作过程中需要给表单赋值情况时,直接通过dom赋值。值的最终状态保持还是用Form的对象实例。
完美解决了原生dom表单不会随着list数据结构的变化而dom变化(采用Form.List),但是不用Form.Item的受控模式管理表单,都用原生表单(值的变化onChange后,写入formInstance),只用Form管理状态。
就是用了Antd的Form组件,但是不用Form.Item包裹,让所有表单项都不受控,再通过Form产生的原生form,来实现状态同步。
import { cloneElement,useEffect, useRef } from 'react';
/**
* 包裹原生表单项,负责初始时表单值到原生表单项的更新
*/
export const CustomFormItem = ({ children, name, form }) => {
const ref = useRef();
useEffect(() => {
const value = form.getFieldValue(name);
switch (ref.current?.type) {
case 'text':
case 'hidden':
ref.current.value = value ?? '';
break;
case 'select-one':
ref.current.value = value ?? '';
break;
case 'checkbox':
ref.current.checked = value;
break;
}
}, []);
return cloneElement(children, { ref, name: name.join('.') });
};
import { Form } from 'antd';
// 给Form表单包裹一层,方便获取原生的form,用于表单值同步
export const CustomForm = ({ form, initialValues, children }) => {
return <div ref={form.initFormRef} className="custom-form-component">
<Form form={form} initialValues={initialValues}>
{children}
</Form>
</div>;
};
import { useRef } from 'react';
import { Form } from 'antd';
import { get } from 'lodash';
/**
* 为解决antd Form表单列表形式中,编辑状态性能很差,
* 故给form实例包裹一层,通过直接操作原生form元素,实现状态同步。
*/
export const useFormCustom = () => {
const [antdForm] = Form.useForm();
const formRef = useRef();
const setElementValue = (name, value) => {
const element = form.getElementByName(name);
switch (element?.type) {
case 'text':
case 'select-one':
case 'hidden':
element.value = value;
break;
case 'checkbox':
element.checked = value;
break;
}
};
const form = {
...antdForm,
initFormRef: ele => {
formRef.current = ele;
},
setFieldsValue: values => {
antdForm.setFieldsValue(values);
},
setFieldValue: (name, value) => {
antdForm.setFieldValue(name, value);
if (name === 'dataSource') {
const _form = formRef.current?.children?.[0];
if (!_form) {
return;
};
// 遍历表单元素并设置值
for (const element of _form.elements) {
setElementValue(element.name, get(value, element.name.split('.').slice(1)));
}
} else {
setElementValue(name, value);
}
},
validateColName: () => {
const _form = formRef.current?.children?.[0];
if (!_form) {
return;
};
const colNameMap = new Map();
for (const element of _form.elements) {
if (element.name.includes('colName')) {
if (element.value) {
if (!colNameMap.has(element.value)) {
colNameMap.set(element.value, {
count: 1,
list: [element],
});
} else {
const item = colNameMap.get(element.value);
item.count++;
item.list.push(element);
}
} else {
element.classList.remove('error-field');
}
}
}
colNameMap.forEach(item => {
if (item.count > 1) {
item.list.forEach(element => {
element.classList.add('error-field');
});
} else {
item.list.forEach(element => {
element.classList.remove('error-field');
});
}
});
colNameMap.clear();
},
getElementByName: name => {
const _form = formRef.current?.children?.[0];
if (!_form) {
return;
};
// 遍历表单元素并设置值
for (const element of _form.elements) {
if (element.name === name) {
return element;
}
}
},
};
return [
form,
];
};
export const useTableColumnHook = ({ tbType, mode }) => {
const [form] = useFormCustom();
const columns = [
{
title: '',
dataIndex: 'key',
fixed: 'left',
width: 60,
render: (v: string, record: unknown,_, index: number) => {
const extraClassName = record.colAction === 'default' ? 'text-gray' : 'table-handle';
if (mode === 'edit') {
return (
<DragHandle extraClassName={extraClassName} index={index + 1} />
);
}
return <span className="ml-2">{index + 1}</span>;
},
},
{
title: (
<span>
字段名称<i className='text-danger'>*</i>
</span>
),
dataIndex: 'colName',
fixed: 'left',
render: (colName: any, record: any, formName) => {
if (mode === 'read' || record.colAction === 'default') {
return <span>{colName}</span>;
}
return <CustomFormItem name={formName} form={form}>
<input
className='custom-input'
onChange={e => {
const newColName = e.target.value.trim();
form.setFieldValue(formName, newColName);
// 验证colName不能重复
form.validateColName();
}}
/>
</CustomFormItem>;
},
},
{
title: (
<span>
字段类型<i className='text-danger'>*</i>
</span>
),
dataIndex: 'colType',
width: 180,
render: (colType: string, record: any, formName) => {
if (mode === 'read' || record.colAction === 'default' || record?.dicId) {
return (
<span>
{FieldsTypeEnum[colType]}({colType})
</span>
);
}
return <CustomFormItem name={formName} form={form}>
<select
name={`${record.id}.colType`}
className={`custom-select ${(record.colAction === 'default' || record?.dicId) ? 'hidden' : ''}`}
onChange={e => {form.setFieldValue(formName, e.target.value);}}
>
{FieldTypeOptions.map(item => {
return <option value={item.value} key={item.value}>{item.label}</option>;
})}
</select>
</CustomFormItem>;
},
},
{
title: '字段显示名',
dataIndex: 'colDisplay',
render: (colDisplay: any, record: any, formName) => {
if (mode === 'read' || record.colAction === 'default') {
return <span>{colDisplay}</span>;
}
if (mode === 'edit') {
return <CustomFormItem name={formName} form={form}>
<input
className='custom-input'
type={'text'}
onChange={e => {form.setFieldValue(formName, e.target.value.trim());}}
/>
</CustomFormItem>;
}
},
},
{
title: '描述',
dataIndex: 'description',
render: (description: string, record: any, formName) => {
if (mode === 'read' || record.colAction === 'default') {
return <span>{description}</span>;
}
if (mode === 'edit') {
return <CustomFormItem name={formName} form={form}>
<input
className='custom-input'
type={'text'}
onChange={e => {form.setFieldValue(formName, e.target.value.trim());}}
/>
</CustomFormItem>;
}
},
},
{
title: '主键',
dataIndex: 'isPrimaryKey',
align: 'center',
width: 60,
render: (isPrimaryKey: Boolean, record: any, formName) => {
if (mode === 'read' || record.colAction === 'default') {
return <span>{isPrimaryKey && <i className="iconfont icon-check-line"></i>}</span>;
}
return <CustomFormItem name={formName} form={form}>
<input
type="checkbox"
className='custom-checkbox'
onChange={e => {
const ele = form.getElementByName([...formName.slice(0, 2), 'isNotNull'].join('.'));
if (ele) {
if (e.target.checked) {
ele.checked = true;
ele.disabled = true;
form.setFieldValue([...formName.slice(0, 2), 'isNotNull'], true);
} else {
ele.disabled = false;
}
}
form.setFieldValue(formName, e.target.checked);
} }
/>
</CustomFormItem>;
},
},
{
title: '非空',
dataIndex: 'isNotNull',
align: 'center',
width: 60,
render: (isNotNull: boolean, record: any, formName) => {
const { isPrimaryKey } = record;
if (mode === 'read' || record.colAction === 'default') {
return <span>{isNotNull && <i className="iconfont icon-check-line"></i>}</span>;
}
return <CustomFormItem name={formName} form={form}>
<input
type="checkbox"
className='custom-checkbox'
disabled={isPrimaryKey}
onChange={e => form.setFieldValue(formName, e.target.checked)}
/>
</CustomFormItem>;
},
},
{
title: '字段类别',
dataIndex: 'colCatalog',
render: (_, record: any, formName) => {
const label = colCatalogOptions.find(op => op.value === record?.colCatalog)?.label;
if (mode === 'read' || record.colAction === 'default') {
return <span>{label}</span>;
}
return <CustomFormItem name={formName} form={form}>
<select
className={`custom-select ${record.colAction === 'default' ? 'hidden' : ''}`}
onChange={e => form.setFieldValue(formName, e.target.value)}
>
{colCatalogOptions.map(({ value, label }) => (
<option value={value} key={value}>{label}</option>
))}
</select>
</CustomFormItem>;
},
},
{
title: '关联类型',
dataIndex: 'indicatorType',
render: (indicatorType: any, record: any, formName) => {
const label = IndicatorTypeOptions.find(op => op.value === indicatorType)?.label;
if (mode === 'read' || record.colAction === 'default') {
return <span>{label}</span>;
}
return <CustomFormItem name={formName} form={form}>
<select
className={`custom-select ${record.colAction === 'default' ? 'hidden' : ''}`}
onChange={e => form.setFieldValue(formName, e.target.value)}
>
{IndicatorTypeOptions.map(({ value, label }) => (
<option value={value} key={value}>{label}</option>))
}
</select>
</CustomFormItem>;
},
},
];
return {
columns,
form,
};
};

浙公网安备 33010602011771号