­

antd4 源碼學習 :表單

Evernote Export

首先。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 ,它沒有也不需要獨立的數據管理,表單容器的數據變化會直接反映到表單
Tags: