Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

各大 Form 大揭秘 -- antd Form #51

Open
SunShinewyf opened this issue Dec 17, 2019 · 0 comments
Open

各大 Form 大揭秘 -- antd Form #51

SunShinewyf opened this issue Dec 17, 2019 · 0 comments

Comments

@SunShinewyf
Copy link
Owner

SunShinewyf commented Dec 17, 2019

各大 Form 大揭秘 -- antd Form

最近的项目涉及到很复杂的 form 交互,包括数据回填、联动等等,使用的是 antd 的 Form 组件,但是在使用过程中遇到了一系列奇怪的问题,所以趁机深入一下 antd Form 的实现,避免日后采坑。同时学习一下其他 Form 的处理方式,比较一下优劣。

Form

antd 的 Form 使用方式如下:

const Example = () => {
  return <div>this is a test</div>
}
const Demo = Form.create()(Example);

 Form.create() 是一个高阶函数,它的源码如下:

 static create = function create<TOwnProps extends FormComponentProps>(
    options: FormCreateOption<TOwnProps> = {},
  ): FormWrappedProps<TOwnProps> {
    return createDOMForm({
      fieldNameProp: 'id',
      ...options,
      fieldMetaProp: FIELD_META_PROP,
      fieldDataProp: FIELD_DATA_PROP,
    });
  };

create 函数直接调用的 rc-form 的 createDOMForm 方法,所以 antd 的 Form 的核心能力基本上都是在 rc-form 中实现的。
而 rc-form 中的 createDOMForm 中的 createDOMForm 直接调用了 createBaseForm,然后传递了一些 mixin 函数进去而已,所以我们直接看 createBaseForm 的源码,在 createBaseForm 中,直接是执行一个函数,也就是高阶函数实现的地方:

function createBaseForm(option = {}, mixins = []){
	...
  return function decorate(WrappedComponent){
    // 实现了一个 Form 组件
    const Form = createReactClass({
      mixins,
      ...
      render(){
     		...
        // 将 form 对象作为 props 传递给外面的组件
        return <WrappedComponent {...props} />;
      }
    })
  }
}

通过上面的源码可以看出,createBaseForm 渲染了 Form.create()(Example) 中传递的外部组件,并且把含有 一系列 API 的 Form 作为 props 传递给了外部组件。

Form 本身的逻辑并不复杂,只是通过传递 layout、labelCol、wrapperCol、onSubmit 等数据来控制整个 Form 的样式渲染以及表单的提交事件等。

FormItem

FormItem 的逻辑也主要是在渲染上,它的使用主要如下所示:

 <Form.Item label="Radio.Button">
   {getFieldDecorator('button')(
     <Radio.Group>
       <Radio.Button value="a">item 1</Radio.Button>
       <Radio.Button value="b">item 2</Radio.Button>
       <Radio.Button value="c">item 3</Radio.Button>
     </Radio.Group>,
   )}
 </Form.Item>

接收一个 children,并渲染。同时执行 renderLabel 和 renderWrapper,也就是渲染 ”label“ 和 ”内容“,这里引入了 antd 自己的栅格布局,每一个 FormItem 都是一个 Row,label 和 wrapper 是 Col。主要代码如下:

 renderChildren(prefixCls: string) {
    const { children } = this.props;
    return [
      this.renderLabel(prefixCls),
      this.renderWrapper(
        prefixCls,
        this.renderValidateWrapper(
          prefixCls,
          children,
          this.renderHelp(prefixCls),
          this.renderExtra(prefixCls),
        ),
      ),
    ];
  }

在 renderWrapper 的时候,同时会去拿每一个 Item 的校验状态,然后根据不同的状态更改 wrapper 的 className,从而控制样式如下样式:
                                            image.png
为啥 FormItem 可以拿到状态呢?接下来就是 Form 核心功能的真正揭秘了

实例化 FieldsStore

前面提到执行 createBaseForm 的时候会返回一个经过 HOC 包装后的组件。这个组件在初始化的时候会执行一系列逻辑,从开始的 getInitialState 看起:

  getInitialState() {
    const fields = mapPropsToFields && mapPropsToFields(this.props);
    this.fieldsStore = createFieldsStore(fields || {});

    this.instances = {};
    this.cachedBind = {};
    this.clearedFieldMetaCache = {};

    this.renderFields = {};
    this.domFields = {};
    ....
    return {
      submitting: false,
    };
  },

从代码可以看出在组件初始化的时候会实例化一个 FieldsStore 的类,这个类主要用来存储表单项的数据和校验状态、文案等。其中,FieldsStore 的 fields 属性主要存储每个表单项的实时状态,结构如下:

this.fields = {
  // 某个表单项
  note: {
    dirty: true, //脏值标识,当字段的值已作变更、但未作校验时,那么脏值标识就为 true;已作校验则置为 false
    errors: undefined, //错误信息
    name: "note", // 字段名
    touched: true, //字段更新标识
    validating: true, //校验状态
    value: "text" // 表单项的值
  }
}

FieldsStore 中的 fieldsMeta 属性用来存储表单项的元数据信息,结构如下:

this.fieldsMeta = {
  // 某个表单项
  note: {
    name: "note", //字段名
    rules: [], // 校验规则
    initialValue: '', // 表单项初始值
    trigger: 'onChange', // 触发的事件名
    validate: [], // 校验规则
    valuePropName: "value" // 值的名称
  }
}

getFieldDecorator

当调用 form.getFieldDecorator 的时候,如下使用:

 <Form.Item label="Note">
        {getFieldDecorator('note', {
          rules: [{ required: true, message: 'Please input your note!' }],
        })(<Input />)}
 </Form.Item>

getFieldDecorator 是一个柯里化函数,用于装饰字段组件,首先传递一个表单项的配置,然后是传递一个组件。在执行 getFieldDecorator 的时候,会去执行 getFieldProps,这个函数主要用于装饰字段组件的 props,在这个函数中,会去设置 FieldsStore 的 fields 和 fieldsMeta 。需要注意的是,在获取 trigger(外部设置的收集子节点的事件,默认是 onChange) 事件的时候,会给事件绑定一个 onCollectValidate 或者 onCollect 回调。对应的代码请移步这里。这就实现了在触发组件的 onChange 的时候,就会去触发绑定的回调。

onCollectValidate 和 onCollect 都调用了 onCollectCommon,onCollectCommon 的代码如下:

onCollectCommon(name, action, args) {
  const fieldMeta = this.fieldsStore.getFieldMeta(name);
  if (fieldMeta[action]) {
    fieldMeta[action](...args);
  } else if (fieldMeta.originalProps && fieldMeta.originalProps[action]) {
    fieldMeta.originalProps[action](...args);
  }
  // 获取新值
  const value = fieldMeta.getValueFromEvent
  ? fieldMeta.getValueFromEvent(...args)
  : getValueFromEvent(...args);
  ...
  const field = this.fieldsStore.getField(name);
  return { name, field: { ...field, value, touched: true }, fieldMeta };
},

在这个函数中,会去重新获取组件的值,并更新 fields 和 fieldsMeta。更新完之后,onCollectValidate 会将该 field 的 dirty 属性置为 true,并调用 validateFieldsInternal 对表单项做校验,在 validateFieldsInternal 中,实例化了一个 async-validator,并拿到 error 信息,更新表单项的校验状态到 fields 中。getFieldDecorator 拿到最新 fields 数据之后,将它作为 props 传递给 FormItem 中包裹的组件,代码如下:

 getFieldDecorator(name, fieldOption) {
   			// 拿到最新的 fields 数据
        const props = this.getFieldProps(name, fieldOption);
        return fieldElem => {
          // We should put field in record if it is rendered
          this.renderFields[name] = true;

          const fieldMeta = this.fieldsStore.getFieldMeta(name);
          const originalProps = fieldElem.props;
          ...
          fieldMeta.originalProps = originalProps;
          fieldMeta.ref = fieldElem.ref;
          // 将数据传给 FormItem 中包裹的组件
          return React.cloneElement(fieldElem, {
            ...props,
            ...this.fieldsStore.getFieldValuePropValue(fieldMeta),
          });
        };
      },

如代码所示,getFieldDecorator 会去拿每次 change 后最新的 fields 数据(包括校验信息),然后将这些数据整合传递给被 FormItem 包裹的组件,这就是上文 FormItem 可以拿到表单项的校验状态的原因了。

setFieldsValue

createBaseForm 中还有一个一个 Api setFieldsValue,它的作用就是用户可以手动设置表单项的值,它里面调用 createBaseForm 的 setFields 方法,setFields 的代码如下:

  setFields(maybeNestedFields, callback) {
    const fields = this.fieldsStore.flattenRegisteredFields(
      maybeNestedFields,
    );
    this.fieldsStore.setFields(fields);
    if (onFieldsChange) {
      const changedFields = Object.keys(fields).reduce(
        (acc, name) => set(acc, name, this.fieldsStore.getField(name)),
        {},
      );
      onFieldsChange(
        {
          [formPropName]: this.getForm(),
          ...this.props,
        },
        changedFields,
        this.fieldsStore.getNestedAllFields(),
      );
    }
    this.forceUpdate(callback);
 },

将新设置的数据更新到 fields 中,然后执行 forceUpdate,强制更新,渲染最新的值。

小结

将上面的流程用时序图表示如下:

image

Form 踩到的一些坑

给未 render 的表单项设置值报错

🌰场景还原

有三个选项,其中,选择选项 b 的时候,显示一个 input 表单项,并手动设置其值,代码如下:

const Example = props => {
  const [radioValue, setRadioValue] = useState('a');
  const { form } = props;
  const { getFieldDecorator } = form;
  
  const onRadioValueChange = e => {
    const value = e.target.value;
    setRadioValue(value);
    // 当选择选项 B 的时候手动设置 input 的值
    value === 'b' && form.setFieldsValue({ 'radio-value': '我是选项b' });
  };
  return (
    <Form>
      <Form.Item label="选项">
        {getFieldDecorator('radio-group', {
          initialValue: 'a',
        })(
          <Radio.Group onChange={onRadioValueChange}>
            <Radio value="a">选项A</Radio>
            <Radio value="b">选项B</Radio>
            <Radio value="c">选项C</Radio>
          </Radio.Group>,
        )}
      </Form.Item>
      {radioValue === 'b' ? (
        <Form.Item label="选项值">
          {getFieldDecorator('radio-value')(<Input />)}
        </Form.Item>
      ) : null}
    </Form>
  );
};

按照上述代码执行,会得到 Form 的 warning 提示:
Warning: You cannot set a form field before rendering a field associated with the value.
并且 Input 表单项正确设置值。

🔎原因诊断

在 setFieldsValue 的时候,会去调用 fieldsStore 的 flattenRegisteredFields 方法:

flattenRegisteredFields(fields) {
    const validFieldsName = this.getAllFieldsName();
    return flattenFields(
      fields,
      path => validFieldsName.indexOf(path) >= 0,
      'You cannot set a form field before rendering a field associated with the value.',
    );
  }

这个方法会拿当前已有的 fieldsName,当切换到选项 B 的时候,Input 表单项还没渲染,导致 fieldsName 并没有记录这个值,所以就会走到这里的校验提示逻辑。更不会成功执行 setFields 操作了。
 

💡如何解决

等 Input 组件渲染完成了再执行 setFieldsValue 操作,如下:

const onRadioValueChange = e => {
    const value = e.target.value;
    setRadioValue(value);
    value === 'b' &&
      Promise.resolve().then(() => {
        form.setFieldsValue({ 'radio-value': '我是选项b' });
      });
  };

对 Form 的感受

整体来说,antd 的 Form 可以让开发者更加便捷,因为里面封装了一些逻辑,使得我们减少重复的劳动。但是也有一些缺点,如下:

  • API 较多,第一次用的人看文档都会望而却步。
  • Form.Item 还是需要写很多的包裹代码,有点冗长。
  • 对联动的支持较差,上面那个案例也说明了这个点。
  • 性能较差,每次更新一个表单项,所有表单都会重新 render
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant