antd4 源码学习 :表单

首先。vue 的数据流是双向的,而 react 的数据流是单向的。

这意味着什么?
这意味着,vue 中,子组件可以用 emit 把数据更新传给父组件。而 react 中, 需要通过父组件把回调函数传给子组件实现类似功能。
 
为什么要说这个?因为框架的设计会影响到组件库的设计。组件库的设计必须配合框架。
 
我们回忆一下, antd3 中表单是怎么用的?
我们需要传入 onSubmit 回调函数,去做表单提交操作。
为什么需要传入这玩意?因为正如上面所说,需要通过父组件把回调函数传给子组件实现类似功能
当我们使用 Form 组件的时候,页面就是父组件。Form 就是子组件。
我们把在页面文件里写的方法传给 Form 的 onSubmit ,它把这个方法绑定在表单的原生提交事件上,实现以上功能。
 

 
我们来看一下 antd3 的 Form.create。
 
Form.create(options)(Component)#
使用方式如下:
class CustomizedForm extends React.Component {}
 
CustomizedForm = Form.create({})(CustomizedForm);
 
然后,我们来看一下 antd3 的 this.props.form.getFieldDecorator(id, options)。

this.props.form.getFieldDecorator(id, options)#

经过getFieldDecorator包装的控件,表单控件会自动添加value(或valuePropName指定的其他属性)onChange(或trigger指定的其他属性),数据同步将被 Form 接管,这会导致以下结果:
不再需要也不应该onChange来做同步,但还是可以继续监听onChange等事件。
你不能用控件的valuedefaultValue等属性来设置表单域的值,默认值可以用getFieldDecorator里的initialValue
你不应该用setState,可以使用this.props.form.setFieldsValue来动态改变表单值。
 
源码:
 
getFieldDecorator: function getFieldDecorator(name, fieldOption) {
        var _this2 = this;
 
 
        var props = this.getFieldProps(name, fieldOption);
        return function (fieldElem) {
          // We should put field in record if it is rendered
          _this2.renderFields[name] = true;
 
 
          var fieldMeta = _this2.fieldsStore.getFieldMeta(name);
          var originalProps = fieldElem.props;
          if (process.env.NODE_ENV !== 'production') {
            var valuePropName = fieldMeta.valuePropName;
            (0, _warning2['default'])(!(valuePropName in originalProps), '`getFieldDecorator` will override `' + valuePropName + '`, ' + ('so please don\'t set `' + valuePropName + '` directly ') + 'and use `setFieldsValue` to set it.');
            var defaultValuePropName = 'default' + valuePropName[0].toUpperCase() + valuePropName.slice(1);
            (0, _warning2['default'])(!(defaultValuePropName in originalProps), '`' + defaultValuePropName + '` is invalid ' + ('for `getFieldDecorator` will set `' + valuePropName + '`,') + ' please use `option.initialValue` instead.');
          }
          fieldMeta.originalProps = originalProps;
          fieldMeta.ref = fieldElem.ref;
          return _react2['default'].cloneElement(fieldElem, (0, _extends6['default'])({}, props, _this2.fieldsStore.getFieldValuePropValue(fieldMeta)));
        };
      },
 
fieldElem 就是我们传进入的表单DOM。
props 是传进去的各种选项(比如表单验证)处理后的东西。
fieldMeta 是把传进去的表单名(比如userName passWord)处理后的东西。
 
getFieldMeta:
function getFieldMeta(name) {
      this.fieldsMeta[name] = this.fieldsMeta[name] || {};
      return this.fieldsMeta[name];
    }
 
getFieldValuePropValue:
function getFieldValuePropValue(fieldMeta) {
      var name = fieldMeta.name,
          getValueProps = fieldMeta.getValueProps,
          valuePropName = fieldMeta.valuePropName;
 
 
      var field = this.getField(name);
      var fieldValue = 'value' in field ? field.value : fieldMeta.initialValue;
      if (getValueProps) {
        return getValueProps(fieldValue);
      }
      return (0, _defineProperty3['default'])({}, valuePropName, fieldValue);
    }
 
cloneElement 是 react 方法。
 
总的来说做了两件事:
  • 把数据放入 Form.state ,便于之后的各种处理。
  • 把传进去的 DOM 进行混入与克隆。
 

 
而在 antd4 中——
 
 
v4 的 Form 不再需要通过Form.create()创建上下文。Form 组件现在自带数据域,因而getFieldDecorator也不再需要,直接写入 Form.Item 即可:
Form 自带表单控制实体,如需要调用 form 方法,可以通过Form.useForm()创建 Form 实体进行操作:
 
那么,什么叫自带数据域
看源码:
ant-design-master\components\form\Form.tsx:
return (
    <SizeContextProvider size={size}>
      <FormContext.Provider value={formContextValue}>
        <FieldForm
          id={name}
          {...restFormProps}
          onFinishFailed={onInternalFinishFailed}
          form={wrapForm}
          className={formClassName}
        />
      </FormContext.Provider>
    </SizeContextProvider>
  );
 
很容易得知 FieldForm 是核心。
 
rc-field-form\es\Form.js:
return React.createElement(Component, Object.assign({}, restProps, {
    onSubmit: function onSubmit(event) {
      event.preventDefault();
      event.stopPropagation();
      formInstance.submit();
    }
  }), wrapperNode);
 
 
参数 Component 是元素名称、wrapperNode 是子元素DOM(常说成children)。
 
Component:
Component = _ref$component === void 0 ? 'form' : _ref$component,
 
wrapperNode:
var wrapperNode = React.createElement(_FieldContext.default.Provider, {
  value: formContextValue
}, childrenNode);
 
_FieldContext 是存放警告信息的,如果一切正常就什么也不做。
 
childrenNode:
var childrenNode = children;
children = _ref.children,
 
我们发现:
  • 当声明 Form 的时候,会渲染 Form 元素。
  • 对于子元素基本上就是什么也不做。
 
为什么呢?
因为还有 Form.Item 。
 
ant-design-master\components\form\FormItem.tsx:
return (
      <Row
        className={classNames(itemClassName)}
        style={style}
        key="row"
        {...omit(restProps, [
          'colon',
          'extra',
          'getValueFromEvent',
          'getValueProps',
          'hasFeedback',
          'help',
          'htmlFor',
          'id', // It is deprecated because `htmlFor` is its replacement.
          'initialValue',
          'isListField',
          'label',
          'labelAlign',
          'labelCol',
          'normalize',
          'preserve',
          'required',
          'validateFirst',
          'validateStatus',
          'valuePropName',
          'wrapperCol',
        ])}
      >
        {/* Label */}
        <FormItemLabel htmlFor={fieldId} required={isRequired} {...props} prefixCls={prefixCls} />
        {/* Input Group */}
        <FormItemInput
          {...props}
          {...meta}
          errors={mergedErrors}
          prefixCls={prefixCls}
          onDomErrorVisibleChange={setDomErrorVisible}
          validateStatus={mergedValidateStatus}
        >
          <FormItemContext.Provider value={{ updateItemErrors: updateChildItemErrors }}>
            {baseChildren}
          </FormItemContext.Provider>
        </FormItemInput>
      </Row>
    );
 
  • baseChildren 只有出错了才会出现,不用管。
  • Form.Item 一定会渲染 Col.
 
关键是 FormItemLabel 和 FormItemInput ,他们都会接收所有的 props 。
 
ant-design-master\components\form\FormItemLabel.tsx:
return (
          <Col {...mergedLabelCol} className={labelColClassName}>
            <label
              htmlFor={htmlFor}
              className={labelClassName}
              title={typeof label === 'string' ? label : ''}
            >
              {labelChildren}
            </label>
          </Col>
        );
 
  • 会渲染 Col.
  • 会渲染 label 元素。文字信息会放在 labelChildren 里面。
ant-design-master\components\form\FormItemInput.tsx:
return (
    <FormContext.Provider value={subFormContext}>
      <Col {...mergedWrapperCol} className={className}>
        <div className={`${baseClassName}-control-input`}>
          <div className={`${baseClassName}-control-input-content`}>{children}</div>
          {icon}
        </div>
        <CSSMotion
          motionDeadline={500}
          visible={visible}
          motionName="show-help"
          onLeaveEnd={() => {
            onDomErrorVisibleChange(false);
          }}
          motionAppear
          removeOnLeave
        >
          {({ className: motionClassName }: { className: string }) => {
            return (
              <div className={classNames(`${baseClassName}-explain`, motionClassName)} key="help">
                {memoErrors.map((error, index) => (
                  // eslint-disable-next-line react/no-array-index-key
                  <div key={index}>{error}</div>
                ))}
              </div>
            );
          }}
        </CSSMotion>
        {extra && <div className={`${baseClassName}-extra`}>{extra}</div>}
      </Col>
    </FormContext.Provider>
  );
 
children 就是某个表单元素,比如 Input 。
 
那么,表单的数据域到底存在于什么地方?它是怎么被声明的?
这必须说到两个东西: createContext useContext.
 
先看这两个东西在 antd4 中是如何被使用的吧。
 
ant-design-master\components\form\FormItem.tsx:
import { FormContext, FormItemContext } from './context';
 
const { name: formName } = React.useContext(FormContext);
const { updateItemErrors } = React.useContext(FormItemContext);
 
ant-design-master\components\form\context.tsx:
export const FormContext = React.createContext<FormContextProps>({
  labelAlign: 'right',
  vertical: false,
  itemRef: (() => {}) as any,
});
 
export interface FormItemContextProps {
  updateItemErrors: (name: string, errors: string[]) => void;
}
 
那么,Context 是个什么玩意?其实,它是 react 的一个机制
官方介绍已经说的很清楚了——
 
在一个典型的 React 应用中,数据是通过 props 属性自上而下(由父及子)进行传递的,但这种做法对于某些类型的属性而言是极其繁琐的(例如:地区偏好,UI 主题),这些属性是应用程序中许多组件都需要的。Context 提供了一种在组件之间共享此类值的方式,而不必显式地通过组件树的逐层传递 props。
 
所以答案已经呼之欲出了。
表单的数据域会存在,而且不需要声明。
因为它用的是 Context ,它没有也不需要独立的数据管理,表单容器的数据变化会直接反映到表单
function getFieldMeta(name) {
      this.fieldsMeta[name] = this.fieldsMeta[name] || {};
      return this.fieldsMeta[name];
    }

 

posted on 2020-07-19 16:19  fox_charon  阅读(837)  评论(0编辑  收藏  举报

导航