form是我们日常使用最多的控件,form是怎么收集表单信息的?是怎么回填数据的,怎么对表单校验的呢?它背后的逻辑和思路是怎样的。研究源码一是可以帮助我们将我们日常工作所使用的控件有一个完整的闭环的理解,二是帮助我们学习到优秀团队对问题的解决思路和方法。
深入源码form.create是form的一个静态方法,它直接调用rc-form的createDOMForm,createDOMForm做了什么?主要是往props下面构建了form的方法及初始化了form的fieldsstore数据。
var mixin = {
getForm: function getForm() {
return (0, _extends3['default'])({}, _createForm.mixin.getForm.call(this), {
validateFieldsAndScroll: this.validateFieldsAndScroll
});
},
validateFieldsAndScroll: function validateFieldsAndScroll(ns, opt, cb) {
var _this = this;
var _getParams = (0, _utils.getParams)(ns, opt, cb),
names = _getParams.names,
callback = _getParams.callback,
options = _getParams.options;
var newCb = function newCb(error, values) {
if (error) {
var validNames = _this.fieldsStore.getValidFieldsName();
var firstNode = void 0;
var firstTop = void 0;
validNames.forEach(function (name) {
if ((0, _has2['default'])(error, name)) {
var instance = _this.getFieldInstance(name);
if (instance) {
var node = _reactDom2['default'].findDOMNode(instance);
var top = node.getBoundingClientRect().top;
if (node.type !== 'hidden' && (firstTop === undefined || firstTop > top)) {
firstTop = top;
firstNode = node;
}
}
}
});
if (firstNode) {
var c = options.container || getScrollableContainer(firstNode);
(0, _domScrollIntoView2['default'])(firstNode, c, (0, _extends3['default'])({
onlyScrollIfNeeded: true
}, options.scroll));
}
}
if (typeof callback === 'function') {
callback(error, values);
}
};
return this.validateFields(names, options, newCb);
}
};
加入了两个函数getForm和validateFieldsAndScroll,传递给createBaseForm,之后在瞧瞧这两个函数的具体作用。
createBaseForm作用是拷贝当前传递下来的组件,是我们常说的高阶组件,也就是调用函数将当前组件传递下去作为被包装组件,最会返回一个具备新的属性的组件。代码太多就不贴代码了。
import React from 'react'
import hoistStatics from 'hoist-non-react-statics';
import createReactClass from 'create-react-class';
// 导出一个
export default function TestDe (){
return function (wa:any){
console.log(wa)
//返回一个根据传递的组件来创建新的元素。
// return React.createElement
// createReactClass 等同于es6的class组件的写法。
// 这样的复制
let forms =createReactClass( {
render(){
console.log('this',this)
return React.createElement(wa,{aaa:123})
},
// 初始化state的数据
getInitialState() {
return {bb:11}
},
}
)
// 功能是拷贝静态方法,
return hoistStatics(forms,wa);
}
}
//高阶函数使用处
import React from 'react';
import './index.less';
import TestDe from '../utils/TestDe';
import { FormComponentProps } from 'antd/es/form';
interface IndexState{
enter?:boolean,
width?:number
}
interface HomeProps extends FormComponentProps{
}
class Home extends React.Component<HomeProps,IndexState> {
static rendertest=()=>{
return 123;
}
render(){
console.log(this);
return (
<div>test </div>
)
}
}
const Ab=TestDe()(Home);
console.log(Ab.rendertest());
export default Ab;
类组件内部的static,当我们没有使用hoistStatics去复制静态方法时,调用会出错。
Form.create(),实质上对我们自己的业务组件进行了一次包装,将Form进行了Form相关属性的初始化和挂在所需要使用的方法等。将一些方法添加到当前props下的form上。
这个函数也可以理解为一个高阶函数,代码,它的作用有两个
getFieldDecorator(name, fieldOption) {
// 在获取字段的props的时候就会设置fieldsStore的字段数据。
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;
if (process.env.NODE_ENV !== 'production') {
const valuePropName = fieldMeta.valuePropName;
warning(
!(valuePropName in originalProps),
`\`getFieldDecorator\` will override \`${valuePropName}\`, ` +
`so please don't set \`${valuePropName}\` directly ` +
`and use \`setFieldsValue\` to set it.`,
);
const defaultValuePropName = `default${valuePropName[0].toUpperCase()}${valuePropName.slice(
1,
)}`;
warning(
!(defaultValuePropName in originalProps),
`\`${defaultValuePropName}\` is invalid ` +
`for \`getFieldDecorator\` will set \`${valuePropName}\`,` +
` please use \`option.initialValue\` instead.`,
);
}
fieldMeta.originalProps = originalProps;
fieldMeta.ref = fieldElem.ref;
// 返回的是原本的props加上添加的一些特殊属性的新的react元素
return React.cloneElement(fieldElem, {
...props,
// 没有给到initvalue的话这个是{value:undefined},又的话是initvalue的值。
...this.fieldsStore.getFieldValuePropValue(fieldMeta),
});
};
},
我们经常使用它去对我们的表单数据做校验,那么他是怎么校验的呢?然后又是怎么渲染错误信息到页面上的呢?
源码中与之相关的是validateFields 和,validateFieldsInternal函数,他们两个之间的话。validateFields 它的功能主要是返回一个promise并且组装validateFieldsInternal所需要的数据,包括所有的字段,字段名。传递callback下去。
_this8.validateFieldsInternal(fields, {
fieldNames: fieldNames,
options: options
}, callback);
validateFieldsInternal它又是做什么的?它是具体对数据fields的值,利用async-validator做校验,并且将校验完毕的错误信息分别存储到与之对应fieldStore中,
validateFieldsInternal: function validateFieldsInternal(fields, _ref, callback) {
var _this7 = this;
var fieldNames = _ref.fieldNames,
action = _ref.action,
_ref$options = _ref.options,
options = _ref$options === undefined ? {} : _ref$options;
var allRules = {};
var allValues = {};
var allFields = {};
var alreadyErrors = {};
// 循环遍历当前的所有字段,
fields.forEach(function (field) {
var name = field.name;
if (options.force !== true && field.dirty === false) {
if (field.errors) {
set(alreadyErrors, name, { errors: field.errors });
}
return;
}
// 获取到对应的fieldMeta,fieldsStore已经具备了,所有字段的属性等参数
var fieldMeta = _this7.fieldsStore.getFieldMeta(name);
var newField = _extends({}, field);
newField.errors = undefined;
newField.validating = true;
newField.dirty = true;
allRules[name] = _this7.getRules(fieldMeta, action);
allValues[name] = newField.value;
allFields[name] = newField;
});
this.setFields(allFields);
// in case normalize
Object.keys(allValues).forEach(function (f) {
allValues[f] = _this7.fieldsStore.getFieldValue(f);
});
if (callback && isEmptyObject(allFields)) {
callback(isEmptyObject(alreadyErrors) ? null : alreadyErrors, this.fieldsStore.getFieldsValue(fieldNames));
return;
}
// 之前的所有操作都是在对当前fields字段的属性做编排然后构成asyncValidator所需要的schama,用于校验数据。
var validator = new AsyncValidator(allRules);
if (validateMessages) {
validator.messages(validateMessages);
}// 校验数据
validator.validate(allValues, options, function (errors) {
var errorsGroup = _extends({}, alreadyErrors);
if (errors && errors.length) {
errors.forEach(function (e) {
// 根据返回的数据转换为fieldsStore所存储的数据,并对fieldsStore对应的字段做是否错误信息的填充,
var errorFieldName = e.field;
var fieldName = errorFieldName;
// Handle using array validation rule.
// ref: https://github.com/ant-design/ant-design/issues/14275
Object.keys(allRules).some(function (ruleFieldName) {
var rules = allRules[ruleFieldName] || [];
// Exist if match rule
if (ruleFieldName === errorFieldName) {
fieldName = ruleFieldName;
return true;
}
// Skip if not match array type
if (rules.every(function (_ref2) {
var type = _ref2.type;
return type !== 'array';
}) || errorFieldName.indexOf(ruleFieldName + '.') !== 0) {
return false;
}
// Exist if match the field name
var restPath = errorFieldName.slice(ruleFieldName.length + 1);
if (/^\d+$/.test(restPath)) {
fieldName = ruleFieldName;
return true;
}
return false;
});
var field = get(errorsGroup, fieldName);
if (typeof field !== 'object' || Array.isArray(field)) {
set(errorsGroup, fieldName, { errors: [] });
}
var fieldErrors = get(errorsGroup, fieldName.concat('.errors'));
fieldErrors.push(e);
});
}
var expired = [];
var nowAllFields = {};
Object.keys(allRules).forEach(function (name) {
var fieldErrors = get(errorsGroup, name);
var nowField = _this7.fieldsStore.getField(name);
// avoid concurrency problems
if (!eq(nowField.value, allValues[name])) {
expired.push({
name: name
});
} else {
nowField.errors = fieldErrors && fieldErrors.errors;
nowField.value = allValues[name];
nowField.validating = false;
nowField.dirty = false;
nowAllFields[name] = nowField;
}
});
// 设置解析完毕的所有的field带上了errors,如果有错误的话。
_this7.setFields(nowAllFields);
// 我们在使用validataFields的回调,获取到的回调返回的东西。
if (callback) {
if (expired.length) {
expired.forEach(function (_ref3) {
var name = _ref3.name;
var fieldErrors = [{
message: name + ' need to revalidate',
field: name
}];
set(errorsGroup, name, {
expired: true,
errors: fieldErrors
});
});
}
callback(isEmptyObject(errorsGroup) ? null : errorsGroup, _this7.fieldsStore.getFieldsValue(fieldNames));
}
});
},
分析到这里,可以得知,在我们对应的fieldsStore中已经存在了我们校验过后的数据了,那么是怎么样渲染出来的呢?
在引用Form的地方我们并没有改变props的属性我们为什么能够触发重新渲染dom结构呢?其实是在我们setFields的时候,调用了Component里面的一个方法。forceUpdate,官网解析当我们使用其他数据去跟新的时候可以使用它去强制重新渲染。
setFields: function setFields(maybeNestedFields, callback) {
var _this4 = this;
var fields = this.fieldsStore.flattenRegisteredFields(maybeNestedFields);
this.fieldsStore.setFields(fields);
if (onFieldsChange) {
var changedFields = Object.keys(fields).reduce(function (acc, name) {
return set(acc, name, _this4.fieldsStore.getField(name));
}, {});
onFieldsChange(_extends(_defineProperty({}, formPropName, this.getForm()), this.props), changedFields, this.fieldsStore.getNestedAllFields());
}
this.forceUpdate(callback);
},
强制更新,就会再次触发getFieldDecorator,得到的field字段数据,这个时候得到的是已经携带errors的数据,
关键代码 Form.Item
renderHelp(prefixCls: string) {
const help = this.getHelpMessage();
const children = help ? (
<div className={`${prefixCls}-explain`} key="help">
{help}
</div>
) : null;
if (children) {
this.helpShow = !!children;
}
return (
<Animate
transitionName="show-help"
component=""
transitionAppear
key="help"
onEnd={this.onHelpAnimEnd}
>
{children}
</Animate>
);
}
getHelpMessage() {
const { help } = this.props;
if (help === undefined && this.getOnlyControl()) {
// 获取错误信息
const { errors } = this.getField();
if (errors) {
return intersperseSpace(
errors.map((e: any, index: number) => {
let node: React.ReactElement<any> | null = null;
if (React.isValidElement(e)) {
node = e;
} else if (React.isValidElement(e.message)) {
node = e.message;
}
// eslint-disable-next-line react/no-array-index-key
return node ? React.cloneElement(node, { key: index }) : e.message;
}),
);
}
return '';
}
return help;
}
//获取孩子的props.data-_field 在之前初始化进去的属性。
getChildProp(prop: string) {
const child = this.getOnlyControl() as React.ReactElement<any>;
return child && child.props && child.props[prop];
}
getId() {
return this.getChildProp('id');
}
getMeta() {
return this.getChildProp(FIELD_META_PROP);
}
// 获取孩子的props
getField() {
return this.getChildProp(FIELD_DATA_PROP);
}
总结来说就是Form.Item获取到getFieldDecorator返回组件的errors信息,然后渲染到页面上。
整个form从初始化到表单收集校验经历了一下几个步骤:
知识点:高阶组件的写法:返回一个函数的封装。如何复制静态方法。