市面上的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
改变的订阅事件,这样应该会好作一点
浅析不了了,头大,断断续续看了一个月,结果今天想总结下,发现都不记得了,只记得一点简单的逻辑,脑袋疼
果真奔着为看源码而看源码并无太大用,仍是得须要作相同的功能再来看源码参考实现比较靠谱