antd源码-form解析(初始化到表单收集校验过程)_antd中的form详解_点了个汤的博客-程序员宝宝

技术标签: antd源码  

antd源码-form解析(初始化到表单收集校验过程)

form是我们日常使用最多的控件,form是怎么收集表单信息的?是怎么回填数据的,怎么对表单校验的呢?它背后的逻辑和思路是怎样的。研究源码一是可以帮助我们将我们日常工作所使用的控件有一个完整的闭环的理解,二是帮助我们学习到优秀团队对问题的解决思路和方法。

Form.create做了什么?

createDOMForm

深入源码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);
  }
};
createBaseForm

加入了两个函数getForm和validateFieldsAndScroll,传递给createBaseForm,之后在瞧瞧这两个函数的具体作用。

createBaseForm作用是拷贝当前传递下来的组件,是我们常说的高阶组件,也就是调用函数将当前组件传递下去作为被包装组件,最会返回一个具备新的属性的组件。代码太多就不贴代码了。

简单实现Form.create()
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干了什么事

这个函数也可以理解为一个高阶函数,代码,它的作用有两个

  1. 是初始化字段数据进入到fieldStore里面去
  2. 第二次获取就是获取fieldStore里面的数据了。
      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 校验

我们经常使用它去对我们的表单数据做校验,那么他是怎么校验的呢?然后又是怎么渲染错误信息到页面上的呢?

源码中与之相关的是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.Item

在引用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从初始化到表单收集校验经历了一下几个步骤:

  1. 通过form.create初始化了许多函数进入props.form,供使用者调用
  2. 通过getFieldDecorator初始化当前表单的属性和值,相当于把我们包裹的input复制了一部分并且附上了value属性,当我们更改input的值,我们就可以收集到这个input的值。
  3. 通过form.item去收集孩子的errors信息,每一个收集到的就是一个校验错误提示,在去渲染

知识点:高阶组件的写法:返回一个函数的封装。如何复制静态方法。

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/luo_qianyu/article/details/105867987

智能推荐

Maven项目的依赖、继承、聚合关系_GENI`USR`OAD的博客-程序员宝宝

目录1.父项目的搭建2.子项目的搭建(继承关系)3.子项目的搭建(聚合关系)4.优雅的使用聚合关系管理jar包版本1.父项目的搭建新建一个Maven Project勾选创建简单项目编辑项目信息以创建一个淘宝项目为例。Group Id写公司名字Artifact Id写父项目名或子模块名Version写版本号以上三个属性为一个项目的坐标,通过这个坐标即可定位一个项目。P...

Matlab中interp1()和interp2()的用法_插值要求每个网格维度至少有两个采样点。_Fillwithstars的博客-程序员宝宝

1.一维插值interp1()yi=interp1(x,y,xi,method)其中,x、y为已知的数据点;xi为想要插值数据点的横坐标,返回对应的纵坐标yi;method为插值方法,总共有四种:1)‘nearest’:最近邻点插值;2)‘linear’:线性插值;3)‘spline’:三次样条插值;4)‘pchip’:保形分段三次插值。2.二维插值interp2()zi=inte...

InheritableThreadLocal_chunwu9349的博客-程序员宝宝

这个类继承ThreadLocal,子类可以从父类继承值:当子线程创建,子线程从父线程的值中继承所有线程本地变量的初始值。通常这些子类的值与父类的值相同;然而,子类可以重写这个类的childValue方法对父类的值做任何操作。可继承线程本地变量用在这种情况中。当线程属性在这种变量中维护时:属...

如何充分利用金仓数据库KingbaseES日志_kingbase日志_沉舟侧畔千帆过_的博客-程序员宝宝

如果配置得当,KingbaseES服务器日志可以成为信息的金矿。诀窍是确定要记录的内容和记录的内容,更重要的是,测试日志是否可以在需要时提供正确的信息。这将是一个反复试验的问题,但我今天在这里讨论的内容应该有一个相当不错的开端。正如我在开始时所说,我非常乐意听到您配置KingbaseES日志记录以获得最佳结果的经验。

jquery-countdown插件_jquery countdown_JOKER-Wing的博客-程序员宝宝

为以后自己学习用~好查找。侵删。[html] view plain copy1.使用:      $('#clock').countdown(finalData[, callback(或options)]);      finalData - 必选      options - 可选    2.参数:      1>fin

随便推点

ORACLE EBS/ERP 采购库存模块分录_ebs 委外加工_jingxiangren的博客-程序员宝宝

<br />ORACLE EBS/ERP 采购库存模块分录第一章 采购模块<br /> <br />一、资产采购(科目来源:库存组织)<br /> <br />1、物料接收<br /> <br />借材料采购接收数量*采购单价<br />贷 应计暂估接收数量*采购单价<br /> <br />2、接收返回供应商<br /> <br />借应计暂估接收数量*采购单价<br />贷材料采购接收数量*采购单价<br /> <br />二、费用采购(科目来源:PO系统选项、手工输入)<br /> <br />1、接

学习经验 | 软考 | 项目管理工程师 | 25天备考一次过_项目管理工程师速成_逸尘谈PM的博客-程序员宝宝

Hello大家好2019年上半年的软考时间已经出来了是5.25距离现在还有两个多月的时间。很多人觉得备考两个多月几天差不多了。小编今天来分享一下上一次备考的经验吧。考试科目:系统集成项目管理工程师考试级别:中级考试时间:2018.11.10上午科目:选择题下午科目:案例分析题备考时间:25天每日学习:工作日3-5小时 周末5-7小时心路历程小编拖延症晚期 于是在9月底报名后 也没有立即行动起来备考 甚至是过了国庆才开始备考。真正开始备考的时候

使用StreamSets将MySQL中变化的数据实时的导入到HBase中_streamsets 3.18.0_象在舞的博客-程序员宝宝

在之前的博客中,小编介绍了如何在CDH中安装StreamSets,文章链接为:《CDH6.3.1中安装StreamSets3.16.0》。透过这个文章名称也能够看出来小编使用的CDH版本以及安装的StreamSets的版本了,这里就不多说了,本文主要介绍如何使用StreamSets实时的将MySQL中的数据导入到HBase中,主要从以下三个方面进行讲解:一、直接将数据实时的导入到HBase;二、选取部分字段进行实时导入;三、使用SQL查询的方式将MySQL中的数据实时的导入到HBase中。...

[ MSF使用实例 ] 利用永恒之蓝(MS17-010)漏洞导致windows靶机蓝屏并获取靶机权限_永恒之蓝会导致蓝屏吗__PowerShell的博客-程序员宝宝

[ MSF使用实例 ] 利用永恒之蓝(MS17-010)漏洞导致windows靶机蓝屏并获取靶机权限永恒之蓝漏洞是方程式组织在其漏洞利用框架中一个针对SMB服务进行攻击的漏洞,该漏洞导致攻击者在目标系统上可以执行任意代码。

jupyter notebook使用教程初学者必备_sereasuesue的博客-程序员宝宝

目录安装与打开jupyter notebook常见命令更改工作路径方法一方法2快捷键Jupyter Notebook如何导入代码安装与打开安装Anaconda会一起打包安装。或者pip 然后打开jupyter,会在默认浏览器(建议Chrome)打开jupyter。jupyter notebook常见命令1、常用的jupyter notebook一些命令如下:指的是在cmd中使用jupyter命令(1)查看jupyter notebook的相关帮助ju.

推荐文章

热门文章

相关标签