市面上的React
表單組件一大堆,都很好使,最近打算開發一個小的ui庫,表單組件本身寫確定很複雜,因而選擇了用Ant Disign
的表單實現,雖然沒本身寫,可是搞清原理仍是重要的react
那麼實際上是一個標題黨了,本文探索的是field-form,衆所周知,Ant Design
的大部分組件都是基於react-component
實現的,Form
組件也是基於rc-field-form
實現的,而後再增長了一些功能,若是可以搞懂rc-field-form
的邏輯,那麼就能知道Ant Design
裏的Form
組件是如何運做的git
<Form>
<Field name="username">
<Input placeholder="Username" />
</Field>
<Field name="password">
<Input placeholder="Password" />
</Field>
</Form>
複製代碼
field-form
的實現仍是基於Context
,若是Context
配合Hooks
使用,在方便的同時有一個比較大的問題,就是它會所有刷新,有一個值改變了,即便組件沒有用這個值,也會刷新,field-form
是怎麼作的呢?github
form-field
本身實現了表單值的存儲、組件的更新,只用了Context
來傳遞方法ide
// useForm.ts
// 建立了處理表單的FormStore實例
const formStore: FormStore = new FormStore(forceReRender);
// 調用getForm()方法獲取方法,以避免一些內部屬性被訪問
formRef.current = formStore.getForm();
複製代碼
// Form.tsx
// 經過useForm獲取formRef.current的值,用context將方法傳遞下去,每個Field字段組件都能獲取到這些方法
<FieldContext.Provider value={formContextValue}>{childrenNode}</FieldContext.Provider>
複製代碼
FormStore
是處理表單的實例,包括整個表單的值Store
,以及更新Store
的方法,那麼FormStore
是一個獨立於React
以外的類,Field
組件須要將刷新組件的方法註冊進FormStore
函數
// Field.tsx
public componentDidMount() {
const { shouldUpdate } = this.props;
// 經過context獲取FormStore實例的內部方法
const { getInternalHooks }: InternalFormInstance = this.context;
const { registerField } = getInternalHooks(HOOK_MARK);
// 註冊當前Field組件實例到Form
this.cancelRegisterFunc = registerField(this);
}
複製代碼
Field
組件是Class
而不是Hooks
,緣由是Hooks
得寫太多額外的代碼了,Class
能夠輕鬆的將整個Field
組件的註冊進FormStore
,這樣在FormStore
裏就能夠調用Field
組件的方法了ui
在值的改變處理這一塊和Redux
很類似,Field
組件的值改變時,派發一個dispatch
,FormStore
收到後改變值,通知全部註冊的Field
組件去對比值,若是須要做出更新就調用React
中的this.forceUpdate()
來強制刷新組件this
首先,得把value
和onChange
傳遞給表單元素組件spa
// Field.tsx
// 建立onChange函數和`value`值的字段
public getControlled = (childProps: ChildProps = {}) => {
// 爲了方便閱讀,刪改了部分代碼
// 當前的namePath
const { getInternalHooks }: InternalFormInstance = this.context;
const { dispatch } = getInternalHooks(HOOK_MARK);
const value = this.getValue();
// children表單元素組件上原本的trigger函數
// 若是children原本就有onChange,被Field受控後仍是會調用的
const originTriggerFunc: any = childProps.onChange;
// 這是要傳給children的props
const control = {
...childProps,
value,
};
// 更改值的函數,默認爲onChange
control.onChange = (...args: EventArgs) => {
// 默認取值是event.target.value
let newValue: StoreValue = defaultGetValueFromEvent(valuePropName, ...args);
// ...
// 派發dispatch,同時FormStore更新store的值
dispatch({
type: 'updateValue',
namePath,
value: newValue,
});
// 調用children原本的函數
if (originTriggerFunc) {
originTriggerFunc(...args);
}
};
// ...
return control;
};
複製代碼
一般組件都是onChange
和value
這兩個受控字段,在getControlled()
方法中,建立了這兩個字段,在onChange
中通知Store
改變,而且還會調用組件上原有的onChange
,同時將組件原有的props
在傳遞下去code
在渲染的時候將這兩個字段傳遞給受控的表單元素組件component
// Field.tsx
// 爲了方便閱讀,刪改了部分代碼
// 渲染Field組件和受控的表單元素組件
public render() {
const { children } = this.props;
let returnChildNode: React.ReactNode = React.cloneElement(
children as React.ReactElement,
//child.props爲參數,同時將onChange和value傳遞給組件
this.getControlled((children as React.ReactElement).props),
);
return <React.Fragment>{returnChildNode}</React.Fragment>;
}
複製代碼
在FormStore
的表單值改變時,還須要通知全部註冊上的Field
組件更新
// useForm.tsx
// class FormStore
// Field組件經過調用dispatch來更新值
private dispatch = (action: ReducerAction) => {
switch (action.type) {
case 'updateValue': {
const { namePath, value } = action;
// 更新值
this.updateValue(namePath, value);
break;
}
// ...
};
// 更新值
private updateValue = (name: NamePath, value: StoreValue) => {
const namePath = getNamePath(name);
const prevStore = this.store;
// 對值進行一個深拷貝
this.store = setValue(this.store, namePath, value);
// 通知註冊的Field組件更新
this.notifyObservers(prevStore, [namePath], {
type: 'valueUpdate',
source: 'internal',
});
// ...
};
// 通知觀察者,也就是通知全部的Field組件
private notifyObservers = (
prevStore: Store,
namePathList: InternalNamePath[] | null,
info: NotifyInfo,
) => {
// 當前store的值和NotifyInfo
const mergedInfo: ValuedNotifyInfo = {
...info,
store: this.getFieldsValue(true),
};
// 獲取全部的字段實例,調用其中的onStoreChange
// 字段實例就是Field組件的React實例,註冊的時候直接將Field的this註冊進來了
this.getFieldEntities().forEach(({ onStoreChange }) => {
onStoreChange(prevStore, namePathList, mergedInfo);
});
};
複製代碼
先是Field
組件值改變,派發dispatch
來改變FormStore
的值,dispatch
內部調用notifyObservers
方法將參數傳遞通知每個Field
組件
因此最後值改變了應該怎麼作仍是回到了Field
組件
// Field.tsx
public onStoreChange: FieldEntity['onStoreChange'] = (prevStore, namePathList, info) => {
const { shouldUpdate } = this.props;
const { store } = info;
const namePath = this.getNamePath();
// 當前字段上一次的值
const prevValue = this.getValue(prevStore);
// 當前字段的值
const curValue = this.getValue(store);
// 當前字段NamePath是否在namePathList中
const namePathMatch = namePathList && containsNamePath(namePathList, namePath);
switch (info.type) {
// ...
default:
// 若是值有改變就刷新組件
if (
namePathMatch ||
dependencies.some(dependency =>
containsNamePath(namePathList, getNamePath(dependency)),
) ||
requireUpdate(shouldUpdate, prevStore, store, prevValue, curValue, info)
) {
this.reRender();
return;
}
break;
}
// ...
};
複製代碼
只包括值是如何改變的基本邏輯就是這樣,仍是蠻繞的
對我來講最大的缺點是沒能提供useField
,在hooks
氾濫的年代,缺乏了這種方式,使用的仍是render props
,多少有點不習慣,可能由於一開始就沒這個需求,因此註冊Field
直接註冊了一個class
實例,除非暴露store
改變的訂閱事件,這樣應該會好作一點
淺析不了了,頭大,斷斷續續看了一個月,結果今天想總結下,發現都不記得了,只記得一點簡單的邏輯,腦袋疼
果真奔着爲看源碼而看源碼並無太大用,仍是得須要作相同的功能再來看源碼參考實現比較靠譜