**
<table><tbody><tr><td bgcolor="#F3F3F3"><font size="4">原创不易,希望能关注下我们,再顺手点个赞~~<font></font></font></td></tr></tbody></table> **
> 想获取更多原创好文,请扫 👆上方二维码关注我们吧~
Antd Form 实现机制解析
背景
> 在中后台业务中,表单页面基础的场景包括组件值的收集、校验和更新。在 to B 业务中,表单页面的需求往往更复杂和定制化,除了上述的基本操作,还要处理包括自定义表单组件、表单联动、表单的异步检验等复杂场景,在一些大型表单页面中还要考虑性能的问题,表单页面的需求往往是新同学摔得第一个跤。 > > 本文分为两个部分,第一部分会通过对 Antd Form 源码的分析来帮助大家对 Form 的整体设计和流程有一个清晰的概念,第二部分会分享一些复杂场景的解决方案。希望可以帮助大家更容易的处理表单需求和快速定位表单场景中的问题。 > > 本文并不涉及过于具体的源码实现分析,大家可以放松心情,一起来对 Form 有一个感性的认知吧~
Form 组件解决了什么问题
首先我们先看一个简单的表单,收集并校验两个组件的值。只需要通过监听两个表单组件的 onChange 事件,获取表单项的 value,根据定义的校验规则对 value 进行检验,生成检验状态和检验信息,再通过 setState 驱动视图更新,展示组件的值以及校验信息即可。
代码实现可能是这样的:
export default class LoginForm extends React.Component {
state = {
username:{
value: '',
error: '',
},
password:{
value: '',
error: '',
},
}
fieldMeta = {
username:{
rules:[],
},
password:{
rules:[],
},
}
onInputChange = (e) => {
const { value,name } = e.target;
// 获取校验结果
const error = this.doValidate(value, name);
this.setState({
[name]:{
value,
error,
}
})
}
validator = (value, rules) => {
...
}
doValidate = (value, name) => {
// 读取校验规则
const { rules } = this.fieldMeta[name];
return validator(value,rules);
}
render() {
const { username, password } = this.state;
return (<div>
<div>
<Input onChange={this.onInputChange} name='username' value={username.value} />
</div>
<div style={errorStyle}>
{username.error}
</div>
<div>
<Input onChange={this.onInputChange} name='password' value={password.value}/>
</div>
<div style={errorStyle}>
{password.error}
</div>
<Button onClick={this.onSubmit}>登录</Button>
</div>);
}
}
用流程图来表示是这样的:
上面的实现,我们设定了一个表单数据状态的模型,来维护组件的 value 和校验的错误信息。
this.state = {
[field1]:{
value: '',
error: '',
},
[field2]:{
value: '',
error: '',
},
...
}
还有一个字段配置相关的模型,维护组件对应的校验规则。
fieldMeta = {
username:{
rules:[]
},
password:{
rules:[],
}
}
对于这种简单的业务场景,上述方式完全可以满足需求。
具体到真实的业务场景,往往更复杂,其中包含多种表单组件,如 Input、Checkbox、Radio、Upload,还有一些自定义表单组件。
这个时候如果继续采用这种方式,不仅需要维护多个 onChange 事件,还要对不同组件 value 的取值做差异化处理,以及对各个组件的校验以及触发时机规则进行维护,就很容易出现“祖传代码”。
对表单场景进行归纳,可以发现每个组件的数据收集、校验、数据更新的流程其实是一致的。对这个流程进行抽象,并且通过一些配置屏蔽组件间的差异性,再对组件的值以及组件的配置规则统一管理,就是我们常见的 Form 表单的解决方案。
Antd Form 是怎么实现的
要实现上面的方案需要解决三个问题:
如何实时收集内部组件的数据?
如何对组件的数据进行校验?
如何更新组件的数据?
下面我们就带着这三个问题,一起看看 Antd Form 是如何来做的吧~
先看一下 Form class 的结构,Form 组件有两个静态属性 Item、createFormField 和一个静态方法 create,Item 是对 FormItem 组件的引用,createFormField 指向 rc-form 提供的同名方法,create 方法则是对 rc-form createDOMForm 的调用,为了方便理解,这边隐藏了部分代码,Form class 整体的结构如下:
import * as React from 'react';
import * as PropTypes from 'prop-types';
import classNames from 'classnames';
import createDOMForm from 'rc-form/lib/createDOMForm';
import createFormField from 'rc-form/lib/createFormField';
import omit from 'omit.js';
import { ConfigConsumer, ConfigConsumerProps } from '../config-provider';
import { tuple } from '../_util/type';
import warning from '../_util/warning';
import FormItem from './FormItem';
import { FIELD_META_PROP, FIELD_DATA_PROP } from './constants';
import FormContext from './context';
const FormLayouts = tuple('horizontal', 'inline', 'vertical');
export default class Form extends React.Component{
static defaultProps = {
colon: true,
layout: 'horizontal',
hideRequiredMark: false,
onSubmit(e: React.FormEvent<HTMLFormElement>) {
e.preventDefault();
},
};
static propTypes = {
prefixCls: PropTypes.string,
layout: PropTypes.oneOf(FormLayouts),
children: PropTypes.any,
onSubmit: PropTypes.func,
hideRequiredMark: PropTypes.bool,
colon: PropTypes.bool,
};
// Item 是对 FormItem 组件的引用
static Item = FormItem;
// createFormField 指向 rc-form 提供的同名方法
static createFormField = createFormField;
// create 方法则是对 rc-form createDOMForm 的调用
static create = function create( options ){
return createDOMForm({
fieldNameProp: 'id',
...options,
fieldMetaProp: FIELD_META_PROP,
fieldDataProp: FIELD_DATA_PROP,
});
};
constructor(props) {
super(props);
}
renderForm = ({ getPrefixCls }: ConfigConsumerProps) => {
const { prefixCls: customizePrefixCls, hideRequiredMark, className = '', layout } = this.props;
const prefixCls = getPrefixCls('form', customizePrefixCls);
const formClassName = classNames(
prefixCls,
{
[`${prefixCls}-horizontal`]: layout === 'horizontal',
[`${prefixCls}-vertical`]: layout === 'vertical',
[`${prefixCls}-inline`]: layout === 'inline',
[`${prefixCls}-hide-required-mark`]: hideRequiredMark,
},
className,
);
const formProps = omit(this.props, [
'prefixCls',
'className',
'layout',
'form',
'hideRequiredMark',
'wrapperCol',
'labelAlign',
'labelCol',
'colon',
]);
return <form {...formProps} className={formClassName} />;
};
render() {
const { wrapperCol, labelAlign, labelCol, layout, colon } = this.props;
return (
<FormContext.Provider
value={{ wrapperCol, labelAlign, labelCol, vertical: layout === 'vertical', colon }}
>
<ConfigConsumer>{this.renderForm}</ConfigConsumer>
</FormContext.Provider>
);
}
}
从 Form 的源码上看,组件本身并不涉及表单数据流程相关的逻辑,Form 组件以及 FormItem 主要处理布局方式、表单样式、属性必填样式、检验文案等视图层面的逻辑。
对数据的收集、校验、更新的流程的抽象以及组件数据管理主要由 rc-form 实现。下面我们继续来看一下核心的 rc-form 模块是怎样的结构。
rc-form 的 核心文件以及核心类图如下:
rc-form 核心逻辑可以从两个文件来看,createBaseForm.js、createFieldsStore.js。
createBaseForm.js 中暴露出的 createBaseForm 函数,创建了一个高阶组件 decorate,decorate 会为我们的目标组件包裹上一个容器组件,也就是上图中的核心类 BaseForm。
createFieldsStore.js 中暴露的 createFieldsStore 函数用来创建 FieldsStore 类的实例。FieldsStore 类可以理解为组件数据的管理中心,负责数据模型的初始化,并提供 Api 对组件数据进行更新和读取,以及获取组件数据的校验结果和数据更改状态。
Form 组件流程分析
我们通过 Antd Pro 中登录页面的实现来一起看一下,Form 内部的调用流程。
首先来看一下 Form 表单的用法:
class CustomizedForm extends React.Component {}
CustomizedForm = Form.create({})(CustomizedForm);
我们有一个自定义组件 CustomizedForm,在使用 Form 表单的时候,我们会先调用 Form.create({})(CustomizedForm)。
初始化阶段
Form.create 函数指向 rc-form 提供的 createBaseForm 方法,createBaseForm 则创建了高阶组件 decorate。
decorate 的参数就是我们的 CustomizedForm 自定义组件。decorate 会创建一个被 BaseForm 组件包裹的自定义表单组件,经过包裹的组件将会自带 this.props.form
属性。为了方便记忆,我们把这个组件称为 FormHocCustomizedForm。
/**
* rc-form/createBaseForm.js
*/
// 默认的数据收集触发事件
const DEFAULT_TRIGGER = 'onChange';
function createBaseForm(option = {}, mixins = []) {
const {
validateMessages,
onFieldsChange,
onValuesChange,
mapProps = identity,
mapPropsToFields,
fieldNameProp,
fieldMetaProp,
fieldDataProp,
formPropName = 'form',
name: formName,
// @deprecated
withRef,
} = option;
// 高阶组件
return function decorate(WrappedComponent) {
const Form = createReactClass({
mixins,
...
render() {
const { wrappedComponentRef, ...restProps } = this.props; // eslint-disable-line
// 为目标组件注入 form props
const formProps = {
[formPropName]: this.getForm(),
};
if (withRef) {
if (
process.env.NODE_ENV !== 'production' &&
process.env.NODE_ENV !== 'test'
) {
warning(
false,
'`withRef` is deprecated, please use `wrappedComponentRef` instead. ' +
'See: https://github.com/react-component/form#note-use-wrappedcomponentref-instead-of-withref-after-rc-form140',
);
}
formProps.ref = 'wrappedComponent';
} else if (wrappedComponentRef) {
formProps.ref = wrappedComponentRef;
}
const props = mapProps.call(this, {
...formProps,
...restProps,
});
return <WrappedComponent {...props} />;
},
});
return argumentContainer(unsafeLifecyclesPolyfill(Form), WrappedComponent);
};
}
export default createBaseForm;
组件创建完成之后,FormHocCustomizedForm 就会经历 React 组件的生命周期。
getInitailState 阶段
Form 并没有通过内部的 state 来管理内部组件的值, 而且创建了 FieldsStore 实例,也就是上面提到的组件数据管理中心。
通过上面的类图我们可以看到 FieldsStore 包含两个属性,fields 和 fieldsMeta,fields主要用来记录每个表单项的实时数据,包含以下属性:
> dirty 数据是否已经改变,但未校验 > > errors 校验文案 > > name 字段名称 > > touched 数据是否更新过 > > value 字段的值 > > validating 校验状态
fieldsMeta 用来记录元数据,即每个字段对应组件的配置信息:
> name 字段的名称 > > originalProps 被 getFieldDecorator() 装饰的组件的原始 props > > rules 校验的规则 > > trigger 触发数据收集的时机 默认 onChange
> > validate 校验规则和触发事件 > > valuePropName 子节点的值的属性,例如 checkbox 应该设为 checked
> > getValueFromEvent 如何从 event 中获取组件的值 > > hidden 为 true 时,校验或者收集数据时会忽略这个字段
Render 阶段
被 Form 管理的组件,需要使用 props.form.getFieldDecorator 来包装,在 Render 阶段需要调用 getFieldDecorator 传入我们的组件配置,包括字段名 name 以及组件元数据 otherOptions,再将字段对应的组件传入 getFieldDecorator 返回的高阶组件。
{getFieldDecorator('name', otherOptions)(<Input />)}
/**
* rc-form/createBaseForm.js
*/
// getFieldDecorator 实际上创建了一个高阶组件,参数是字段对应的组件
getFieldDecorator(name, fieldOption) {
// 装饰组件的 props
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;
return React.cloneElement(fieldElem, {
...props,
...this.fieldsStore.getFieldValuePropValue(fieldMeta),
});
};
},
经过 getFieldDecorator
包装的组件,表单组件会自动添加 value
(或 valuePropName
指定的其他属性) onChange
(或 trigger
指定的其他属性)属性,接下来的数据同步将被 Form 接管。 getFieldDecorator 主要用于装饰组件,其中调用的 getFieldProps 用于装饰 props,getFieldProps 会将组件的 (DEFAULT_TRIGGER = 'onChange')触发事件指向 FormHocCustomizedForm 的 onCollect 方法,并将配置的 validateTriggers 指向 onCollectValidate。在这个阶段还会收集组件的元数据,也就是我们调用 getFieldDecorator 中传入的 option 配置,这些配置会存入 fieldStore 的 fieldsMeta 对象中,作为组件的元数据。
这里我们就可以回答第一个问题,如何实时收集内部组件的数据?
> Form 通过 getFieldDecorator
对组件进行包装,接管组件的 value 和 onChange
属性,当用户输入改变时,触发 onCollect 或 onCollectValidate 来收集组件最新的值。
用户输入
当用户输入触发组件的 onChange 或者其他的 trigger 事件时,执行 onCollect 或者 onCollectValidate,onCollect 执行组件数据收集,onCollectValidate 除了执行组件数据收集,还会根据配置的校验规则来校验组件,其中校验的流程并不由 rc-form 实现,而且通过引入第三方校验库 async-validator 执行。
onCollect 和 onCollectValidate 方法中收集数据的动作主要由 onCollectCommon 来处理。
/**
* rc-form/createBaseForm.js
*/
onCollect(name_, action, ...args) {
const { name, field, fieldMeta } = this.onCollectCommon(
name_,
action,
args,
);
const { validate } = fieldMeta;
this.fieldsStore.setFieldsAsDirty();
const newField = {
...field,
dirty: hasRules(validate),
};
this.setFields({
[name]: newField,
});
},
onCollectCommon 负责组件数据的收集,在事件的回调中,通过默认的 getValueFromEvent 方法或者组件配置的 getValueFromEvent 方法,可以从参数 event 中正确的拿到组件的值。
/**
* rc-form/createBaseForm.js
*/
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);
if (onValuesChange && value !== this.fieldsStore.getFieldValue(name)) {
const valuesAll = this.fieldsStore.getAllValues();
const valuesAllSet = {};
valuesAll[name] = value;
Object.keys(valuesAll).forEach(key =>
set(valuesAllSet, key, valuesAll[key]),
);
onValuesChange(
{
[formPropName]: this.getForm(),
...this.props,
},
set({}, name, value),
valuesAllSet,
);
}
const field = this.fieldsStore.getField(name);
return { name, field: { ...field, value, touched: true }, fieldMeta };
},
针对不同的组件取值差异,由 getValueFromEvent 方法来屏蔽。
/**
* rc-form/utils.js
*/
// 默认的 getValueFromEvent
export function getValueFromEvent(e) {
// To support custom element
if (!e || !e.target) {
return e;
}
const { target } = e;
return target.type === 'checkbox' ? target.checked : target.value;
}
收集并校验组件的值。
/**
* rc-form/createBaseForm.js
*/
onCollectValidate(name_, action, ...args) {
const { field, fieldMeta } = this.onCollectCommon(name_, action, args);
// 获取组件最新的值
const newField = {
...field,
dirty: true,
};
this.fieldsStore.setFieldsAsDirty();
// 对组件最新的值 进行校验
this.validateFieldsInternal([newField], {
action,
options: {
firstFields: !!fieldMeta.validateFirst,
},
});
},
执行校验。
validateFieldsInternal(
fields,
{ fieldNames, action, options = {} },
callback,
) {
const allRules = {};
const allValues = {};
const allFields = {};
const alreadyErrors = {};
fields.forEach(field => {
const name = field.name;
if (options.force !== true && field.dirty === false) {
if (field.errors) {
set(alreadyErrors, name, { errors: field.errors });
}
return;
}
const fieldMeta = this.fieldsStore.getFieldMeta(name);
const newField = {
...field,
};
newField.errors = undefined;
newField.validating = true;
newField.dirty = true;
// 从 fieldMeta 中读取校验规则
allRules[name] = this.getRules(fieldMeta, action);
allValues[name] = newField.value;
allFields[name] = newField;
});
// 校验前更新字段状态
this.setFields(allFields);
// in case normalize
Object.keys(allValues).forEach(f => {
allValues[f] = this.fieldsStore.getFieldValue(f);
});
// AsyncValidator 三方校验库 async-validator;
const validator = new AsyncValidator(allRules);
if (validateMessages) {
validator.messages(validateMessages);
}
validator.validate(allValues, options, errors => {
const errorsGroup = {
...alreadyErrors,
};
if (errors && errors.length) {
errors.forEach(e => {
// 省略...
const fieldErrors = get(errorsGroup, fieldName.concat('.errors'));
fieldErrors.push(e);
});
}
const expired = [];
const nowAllFields = {};
Object.keys(allRules).forEach(name => {
const fieldErrors = get(errorsGroup, name);
const nowField = this.fieldsStore.getField(name);
// avoid concurrency problems
if (!eq(nowField.value, allValues[name])) {
expired.push({
name,
});
} else {
nowField.errors = fieldErrors && fieldErrors.errors;
nowField.value = allValues[name];
nowField.validating = false;
nowField.dirty = false;
nowAllFields[name] = nowField;
}
});
// 检验完成 更新字段实时数据
this.setFields(nowAllFields);
// ...
});
},
到这里我们可以回答上面的第二个问题,如何对组件的数据进行校验?
> 当通过执行 onCollectCommon 完成了表单数据的收集,onCollectValidate 会调用 validateFieldsInternal 方法创建 AsyncValidator 的实例,由 AsyncValidator 根据组件的配置规则进行校验,并将最终的校验结果和表单数据更新到 fieldStore。
到这里就完成了表单数据的收集和校验的环节,已经拿到了表单最新的数据以及校验结果。
下一步,就是数据的更新,也就是将表单最新的值和校验相关的信息更新到视图上。
在 onCollect 和 validateFieldsInternal 方法中,我们看到最后一步调用了 setFields 来更新实时数据。
/**
* rc-form/createBaseForm.js
*/
setFields(maybeNestedFields, callback) {
const fields = this.fieldsStore.flattenRegisteredFields(
maybeNestedFields,
);
// 更新 fieldsStore
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);
},
setFields 方法将字段组件最新的数据更新到 fieldStore。此时 fieldStore 已经收集存储了组件最新的值,下面我们就需要更新组件,将数据正确的在界面上渲染出来。
可以看到,setFields 中最后调用了 React 组件提供的 forceUpdate 函数。这里可以回答第三个问题,如何更新组件的数据?
> 因为我们在最顶层的 FormHocCustomizedForm 组件中调用 forceUpdate,forceUpdate 会跳过 shouldComponentUpdate 触发组件的 Render 方法,进而触发所有子组件的更新流程。在子组件 Render 的执行过程中, getFieldDecorator 方法从 fieldStore 中读取实时的表单数据以及校验信息,并通过注入 value 或者 valuePropName
的值设定的属性来更新表单。
到这里,一个完整的 Form 数据收集、校验、更新流程就完成了,整个过程的流程图如下所示:
复杂表单场景的最佳实践
看完了上面的 Form 内部的运行流程,下面我们一起来看看 Form 还提供了哪些机制方便我们解决一些复杂场景问题。
嵌套数据结构收集
FieldStore 内部集成了 lodash/set,可以设置对象路径(eg:a.b.c 或者 a.b[0])为字段值,通过使用对象路径字段,我们可以很方便的实现嵌套数据结构值的收集。
<FormItem>
<Col span={16}>
{getFieldDecorator('nested.fieldObj.name')(<Input/>)}
</Col>
</FormItem>
<FormItem>
<Col span={16}>
{getFieldDecorator('nested.fieldArray[0].name')(<Input/>)}
</Col>
</FormItem>
上面的代码中,我们通过对象路径的方式来设置 field,在获取表单值的时候已经被转换成了对应路径结构的对象或数组,如下面所示:
{
nested:{
fieldObj:{
name:'嵌套对象的值'
},
fieldArray:['嵌套数组的值']
}
}
自定义表单接入
上面的分析里提到,Form 通过接管组件的 value 和 onChange 事件来管理组件,想实现一个可以接入 Form 管理的组件,只需要满足下面三个条件
- 提供受控属性
value
或其它与valuePropName
的值同名的属性 - 提供
onChange
事件或trigger
的值同名的事件 - 支持 ref:
- React@16.3.0 之前只有 Class 组件支持
- React@16.3.0 及之后可以通过 forwardRef 添加 ref 支持(示例)
表单联动
组件的数据由 FieldStore 来统一管理,组件值变化时也会实时更新,所以结合 ES6 的 get 方法可以很简单的实现组件之间的联动。
class Linkage extends Component{
get showInput(){
return this.props.form.getFieldValue('checkbox');
}
render() {
const { form } = this.props;
const { getFieldDecorator } = form;
return (
<div>
{
getFieldDecorator('checkbox', {valuePropName: 'checked',})(
<Checkbox>show Input</Checkbox>
)
}
{
this.showInput && <Input/>
}
</div>
);
}
}
export default Form.create()(Linkage);
总结
本文在流程上对 Form 组件的实现机制进行了解析,省略了里面的实现细节,大家对流程有一个整体认知之后,也可以自己翻阅 Form 的源码来了解实现细节。
Antd Form 具有很好的灵活性,可以帮我们快速的实现表单需求,但是也存在一些问题,比如当表单中的任何一个组件值发生改变,触发 onCollect 数据收集、执行更新流程,都会调用 forceUpdate 触发所有组件的更新。
在复杂表单业务,用户频繁的输入场景就会产生性能瓶颈。对于复杂的表单组件,我们可以通过拆分组件的粒度,通过 shouldComponentUpdate 来避免不必要的更新,或者修改组件的数据收集时机来减少数据的收集频率。当然这并不是很优雅的解决方案,在未来要发布的 Antd V4 版本中,Form 的底层实现已经替换为 rc-field-form,主页上的介绍是:
> React Performance First Form Component.
大家也可以期待一下官方新版本的 Form 组件。