1. 程式人生 > 實用技巧 >antd4 原始碼學習 :表單

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); (
關於createElement方法...
) 引數 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 ,它沒有也不需要獨立的資料管理,表單容器的資料變化會直接反映到表單