列表编辑性能优化

公司现有系统有一个字段管理功能,该功能编辑表的字段,数据结构是列表。每一条数据大概有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,
  };
};

 

posted @ 2025-05-15 17:46  冰狐2009  阅读(35)  评论(0)    收藏  举报