各大 Form 大揭秘 -- antd Form
SunShinewyf opened this issue · 0 comments
各大 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,从而控制样式如下样式:
为啥 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,强制更新,渲染最新的值。
小结
将上面的流程用时序图表示如下:
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