rc-form源码浅析
rc-form源码浅析
最近在react工程中用到了antd,想学习一下其中form表单的实现原理;antd的form组件是在rc-form的基础上进行封装的,因此本文主要就以下几个点针对性的看了一下rc-form的源码。
- 在每次使用form时,为什么必须要用form.create包一层
- form表单如何实现数据托管,getFieldDeractor干了什么
- 如何实现双向绑定
- 如何强制更新表单的值
- 如何进行校验
form的使用方式
antd form的使用方式如下,针对每个表单组件使用getFieldDeractor函数包一下,getFieldDeractor看着像是一个高阶组件,传入input组件并返回新的组件,在底部使用Form.create包一下整个表单。在初期使用的时候,经常会碰到一个报错,读取不到getFieldDecorator, 检查发现是漏写了Form.create。那么问题来了,这个Form.create是干什么的?
<Form layout="inline" onSubmit={this.handleSubmit}>
<Form.Item validateStatus={usernameError ? 'error' : ''} help={usernameError || ''}>
{getFieldDecorator('username', {
rules: [{ required: true, message: 'Please input your username!' }],
<Input
prefix={<Icon type="user" style={{ color: 'rgba(0,0,0,.25)' }} />}
placeholder="Username"
</Form.Item>
</Form>
const WrappedHorizontalLoginForm = Form.create({ name: 'horizontal_login' })(HorizontalLoginForm);
form.create的实现
首先从Form.create看起!感觉搞了什么事情……3.x版本的antd form使用时都要用Form.create包裹一下,不然会经常出现this.props.form不存在的情况。先来看看form.create都干了些什么
export default class Form extends React.Component<FormProps, any> {
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,
}
在Form的代码里面有一个静态方法create,在返回时调用了rc-form里面的createDomForm。这个方法把mixin里面的一些方法作为参数,传入createBaseForm方法
function createForm(options) {
// 把mixin传进去
return createBaseForm(options, [mixin]);
export const mixin = {
getForm() {
return {
getFieldsValue: this.fieldsStore.getFieldsValue,
getFieldValue: this.fieldsStore.getFieldValue,
getFieldInstance: this.getFieldInstance,
setFieldsValue: this.setFieldsValue,
setFields: this.setFields,
setFieldsInitialValue: this.fieldsStore.setFieldsInitialValue,
getFieldDecorator: this.getFieldDecorator,
getFieldProps: this.getFieldProps,
getFieldsError: this.fieldsStore.getFieldsError,
getFieldError: this.fieldsStore.getFieldError,
isFieldValidating: this.fieldsStore.isFieldValidating,
isFieldsValidating: this.fieldsStore.isFieldsValidating,
isFieldsTouched: this.fieldsStore.isFieldsTouched,
isFieldTouched: this.fieldsStore.isFieldTouched,
isSubmitting: this.isSubmitting,
submit: this.submit,
validateFields: this.validateFields,
resetFields: this.resetFields,
};
这里的mixin是一个getForm方法,里面返回了例如getFieldDecorator、getFieldsValue等一些常见的,需要通过this.props.form得到的方法,接下来看看createBaseForm
function createBaseForm(option = {}, mixins = []) {
return function decorate(WrappedComponent) {
var Form = createReactClass({
getInitialState: function getInitialState() {
var _this = this;
// 每个Form都有自己的fieldsStore,用来存储一些form相关的
var fields = mapPropsToFields && mapPropsToFields(this.props);
this.fieldsStore = createFieldsStore(fields || {});
// (一堆api……)
render() {
const { wrappedComponentRef, ...restProps } = this.props; // eslint-disable-line
// 这里是form的一些属性
const formProps = {
// formPropName,默认值是form
[formPropName]: this.getForm(),
const props = mapProps.call(this, {
...formProps,
...restProps,
return <WrappedComponent {...props} />; // 原有属性和form属性一起返回给组件
return argumentContainer(unsafeLifecyclesPolyfill(Form), WrappedComponent);
}
这里通过调用createBaseForm,返回了一个高阶函数decorate,这个函数接收一个组件并返回一个组件。这里通过属性代理(Props Proxy)的方式把原有的props和新的props一起返回给组件,返回的新组件通过argumentContainer包裹,函数使用了一个库
hoist-non-react-statics,
主要是为了拷贝传入组件的一些静态方法到新组件上(
react文档
有提到过)
import hoistStatics from 'hoist-non-react-statics';
export function argumentContainer(Container, WrappedComponent) {
/* eslint no-param-reassign:0 */
Container.displayName = 'Form(' + getDisplayName(WrappedComponent) + ')';
Container.WrappedComponent = WrappedComponent;
return hoistStatics(Container, WrappedComponent);
}
总结一下form.create整体流程
1、调用Form.create()来创建一个表单 2、Form初始化了一个只属于该组件实例的 fieldStore ,用来存放当前Form组件的一些输入的数据
其中fileldsStore中包含了当前form的主要信息, fieldMeta,可以看成是一个表单项的描述,即配置信息不涉及值的问题:
- name 字段的名称
- originalProps 被 getFieldDecorator() 装饰的组件的原始 props
- rules 校验的规则
-
trigger 触发数据收集的时机 默认
onChange
- validate 校验规则和触发事件
-
valuePropName 子节点的值的属性,例如 checkbox 应该设为
checked
- getValueFromEvent 如何从 event 中获取组件的值
- hidden 为 true 时,校验或者收集数据时会忽略这个字段
fields主要用于记录每个表单的实时属性,主要包括:
- dirty 数据是否已经改变,但未校验
- errors 校验文案
- name 字段名称
- touched 数据是否更新过
- value 字段的值
- validating 校验状态
这里的方法有点多,可以根据一张图来参考下
数据托管:getFieldDeractor
getFieldDeractor主要目的是“托管”,表单控件会自动添加 value(或 valuePropName 指定的其他属性)onChange(或 trigger 指定的其他属性),其内部实现如下:
getFieldDecorator(name, fieldOption) {
// 获取表单属性,会根据传入的表单项,更新(没有则会创建)fielsStore里的FieldMeta,
// 返回表单项属性。会包含value等属性,绑定onChange事件。
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;
// 返回clone元素,并且把新的属性以及onChange事件都加上
return React.cloneElement(fieldElem, {
...props,
...this.fieldsStore.getFieldValuePropValue(fieldMeta),
}
getFieldsProps获取新组件的props
getFieldDeractor主要是通过调用getFieldsProps方法,为返回后的组件增加了一些表单属性,绑定了onChange对应的事件用于组件更新等;getFieldProps方法里面进行了设置初始值、绑定value、以及通过是否需要校验来判断onChange绑定不同的事件等操作
getFieldProps(name, usersFieldOption = {}) {
const fieldOption = {
name,
trigger: DEFAULT_TRIGGER, // DEFAUKT_TRIGGER的默认值为onChange
valuePropName: 'value',
validate: [],
...usersFieldOption,
const {
rules,
trigger,
validateTrigger = trigger,
validate,
} = fieldOption;
const fieldMeta = this.fieldsStore.getFieldMeta(name);
// 将初始值设置到fieldMeta中
if ('initialValue' in fieldOption) {
fieldMeta.initialValue = fieldOption.initialValue;
const inputProps = {
...this.fieldsStore.getFieldValuePropValue(fieldOption),
ref: this.getCacheBind(name, `${name}__ref`, this.saveRef),
if (fieldNameProp) {
inputProps[fieldNameProp] = formName ? `${formName}_${name}` : name;
// 获取表单项的校验规则
const validateRules = normalizeValidateRules(
validate,
rules,
validateTrigger,
const validateTriggers = getValidateTriggers(validateRules);
// 当需要校验时,onChange事件绑定this.onCollectValidate
validateTriggers.forEach(action => {
if (inputProps[action]) return;
inputProps[action] = this.getCacheBind(
name,
action,
this.onCollectValidate,
// 不需要校验时onChange对应onCollect
if (trigger && validateTriggers.indexOf(trigger) === -1) {
inputProps[trigger] = this.getCacheBind(
name,
trigger,
this.onCollect,
const meta = {
...fieldMeta,
...fieldOption,
validate: validateRules,
// 保存到fieldStore中
this.fieldsStore.setFieldMeta(name, meta);
if (fieldMetaProp) {
inputProps[fieldMetaProp] = meta;
if (fieldDataProp) {
inputProps[fieldDataProp] = this.fieldsStore.getField(name);
// This field is rendered, record it
this.renderFields[name] = true;
return inputProps;
},
getFieldValuePropValue获取value
可以注意到getFieldDecorator底部调用了调用了getFieldValuePropValue来设置value的值,这就引发了3.x版本的一个initialValue受控的问题。问题在于:假如一个表单设置了initialValue && 这个表单还没有任何输入 && initialValue的值受控,你就会发现表单的值随着initialValue的改变一起跟着变了,这是由于在赋值时,会先检测fieldStore中有没有这个组件的值,没有的话就会取initialValue的值。
// 这个函数式在getProps的时候调用的
function getFieldValuePropValue(fieldMeta) {
var name = fieldMeta.name,
getValueProps = fieldMeta.getValueProps,
valuePropName = fieldMeta.valuePropName;
var field = this.getField(name);
// 注意这里,返回的是field的value,没有的话则使用initValue
var fieldValue = 'value' in field ? field.value : fieldMeta.initialValue;
if (getValueProps) {
return getValueProps(fieldValue);
return _defineProperty({}, valuePropName, fieldValue);
}
总结一下:
- 用户通过getFieldDecorator给表单传入一些配置项,key/初始值/校验规则等
- getDecorator会通过调用getFieldProps来创建/更新对应的fieldStore, 生成新的props,并根据是否需要校验给onChange绑定不同的方法。最终讲这些信息传入到clone的表单组件返回
表单如何实现双向绑定
表单数据更新时onChange绑定的事件及流程如下:
正常我们在react实现双向绑定通常是通过onChange事件,form表单中通过getFieldDecorator来接管双向绑定及一系列复杂的校验工作;在前面我们提到,在进行初始化表单属性时(getFieldProps),对没有rules的表单绑定了onCollent事件,需要校验的绑定了onCollectValidate方法,他们都主要调用了
onCollectCommon
方法,并将得到的结果保存到fieldStore。
// getFieldProps中的事件绑定
const validateTriggers = getValidateTriggers(validateRules);
validateTriggers.forEach(action => {
if (inputProps[action]) return;
inputProps[action] = this.getCacheBind(
name,
action,
this.onCollectValidate,
if (trigger && validateTriggers.indexOf(trigger) === -1) {
inputProps[trigger] = this.getCacheBind(
name,
trigger,
this.onCollect,
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(name, action, args) {
const fieldMeta = this.fieldsStore.getFieldMeta(name);
// 如果getFieldDecorator方法中的参数定义了onChange,则触发该onChange
if (fieldMeta[action]) {
fieldMeta[action](...args);
// 如果输入组件绑定了onChange,则触发该onChange
else if (fieldMeta.originalProps && fieldMeta.originalProps[action]) {
fieldMeta.originalProps[action](...args);
// 获取事件中更新后的value
const value = fieldMeta.getValueFromEvent ?
fieldMeta.getValueFromEvent(...args) :
getValueFromEvent(...args);
if (onValuesChange && value !== this.fieldsStore.getFieldValue(name)) { // 如果Form.create时用户定义有onValuesChange,则触发
const valuesAll = this.fieldsStore.getAllValues();
const valuesAllSet = {};
valuesAll[name] = value;
Object.keys(valuesAll).forEach(key => set(valuesAllSet, key, valuesAll[key]));
onValuesChange(this.props, set({}, name, value), valuesAllSet);
const field = this.fieldsStore.getField(name); // 获取合并field,并返回
return ({ name, field: { ...field, value, touched: true }, fieldMeta });
},
当表单数据变化是会触发onChange,调用对应的onCollect方法,在起内部通过调用onCollectCommon来执行对应的onChange,并获取事件中更新后的值,生成新的field,然后调用setFields触发更新;
如何强制更新表单的值
我们可以通过setFieldsValue来强制更新表单的值,具体实现如下:
setFieldsValue(changedValues, callback) {
const { fieldsMeta } = this.fieldsStore;
const values = this.fieldsStore.flattenRegisteredFields(changedValues);
const newFields = Object.keys(values).reduce((acc, name) => {
}, {});
// 调用setFields更新fieldStore中的值
this.setFields(newFields, callback);
// 如果设置了全局onValuesChange事件,则触发
if (onValuesChange) {
const allValues = this.fieldsStore.getAllValues();
onValuesChange(
[formPropName]: this.getForm(),
...this.props,
changedValues,
allValues,
setFields(maybeNestedFields, callback) {
const fields = this.fieldsStore.flattenRegisteredFields(
maybeNestedFields,
// 更新filedStore中的值
this.fieldsStore.setFields(fields);
// 触发强制更新
this.forceUpdate(callback);
}
由于antd组件统一维护了一个filedStore,没有涉及到state, 所以所有涉及到数据更新的都需要先对fieldStore里面的fileds更新后,再手动触发更新;强制触发更新会重新执行getFieldDeractor,整个过程和最初创建时相同,只是此时fieldStore里对应的field已经有值了,在执行时我们拿到了这个value,在cloneElement的时候,加在了元素的属性上,最终元素进行了更新。 但是,每次通过setFields触发forceUpdate会有性能问题,每次涉及到表单内的值改变,都会整个渲染一遍,性能大大降低,毕竟全部重新渲染了一遍……
如何进行校验
前面提到,如果配置了validateTrigger的组件,会绑定onCollectValidate事件,实现如下:
onCollectValidate(name_, action, ...args) {
// 从onCollectCommon中获取更新后的值
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,
获取校验规则等
// 主要使用async-validator来执行具体的校验工作
const validator = new AsyncValidator(allRules);
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;