写在前面
对于经常使用 react 开发的朋友来说,antd 应该不陌生。开发中经常遇到的表单大多会使用 antd 中的 Form 系列组件完成,而 rc-field-form 又是 antd Form 的重要组成部分,或者说 antd Form 是对 rc-field-form 的简单封装。
本人也是在开发中遇到了极其复杂的表单结构,用寻常方式开发十分蛋疼,因此深入了解了一下 rc-field-form 的工作原理,希望另辟蹊径,结合 rc-field-form 的能力简化开发复杂度。这篇文章分享一下我对 rc-field-form 内部原理的认识。
rc-field-form 有几个比较重要的组成部分,FormStore、Form 组件 和 Field(FormItem) 组件,再利用 React Context 的能力,把这几个部分结合在一起,就组成了表单的基本功能。
FormStore
在说 FormStore 之前先说说 FormInstance。大家都知道在调用 useForm 钩子后会得到一个包含各种 form 操作函数的对象,我们把这个对象称为 Form 实例。
const [form] = Form.useForm();
而 FormStore 是一个类,存储了 Form 表单数据,并定义了各种对数据的操作。当我们调用 useForm 时,内部就会创建一个 FormStore 的实例,并通过 useRef 缓存起来,也就是说在调用 useForm 的组件创建直到销毁这段时间内,无论组件渲染多少次,FormStore 只会实例化一次。
/* useForm 源码 */
function useForm<Values = any>(form?: FormInstance<Values>): [FormInstance<Values>] {
const formRef = React.useRef<FormInstance>();
const [, forceUpdate] = React.useState();
if (!formRef.current) {
if (form) {
formRef.current = form;
} else {
// Create a new FormStore if not provided
const forceReRender = () => {
forceUpdate({});
};
const formStore: FormStore = new FormStore(forceReRender); // 实例化 FormStore
formRef.current = formStore.getForm(); // 获得 Form 实例
}
}
return [formRef.current];
}
最终得到的 Form 实例实际上是 formStore.getForm() 的返回值,这里面只暴露了一些 public 方法,把 FormStore 的其他属性隐藏,这样外部在使用时就比较安全。当然,如果我们传入了一个 form,就会直接返回我们传入的这个 form。
/* FormStore.getForm 源码 */
class FormStore {
// ... 其他属性和方法
public getForm = (): InternalFormInstance => ({
getFieldValue: this.getFieldValue,
getFieldsValue: this.getFieldsValue,
getFieldError: this.getFieldError,
getFieldsError: this.getFieldsError,
isFieldsTouched: this.isFieldsTouched,
isFieldTouched: this.isFieldTouched,
isFieldValidating: this.isFieldValidating,
isFieldsValidating: this.isFieldsValidating,
resetFields: this.resetFields,
setFields: this.setFields,
setFieldsValue: this.setFieldsValue,
validateFields: this.validateFields,
submit: this.submit,
getInternalHooks: this.getInternalHooks,
});
private getInternalHooks = (key: string): InternalHooks | null => {
if (key === HOOK_MARK) {
this.formHooked = true;
return {
dispatch: this.dispatch,
registerField: this.registerField,
useSubscribe: this.useSubscribe,
setInitialValues: this.setInitialValues,
setCallbacks: this.setCallbacks,
setValidateMessages: this.setValidateMessages,
getFields: this.getFields,
setPreserve: this.setPreserve,
};
}
warning(false, '`getInternalHooks` is internal usage. Should not call directly.');
return null;
};
}
看到这里相信大家就比较熟悉了,这些方法就是我们日常使用 Form 实例的常见方法,但和 antd 中的 Form 实例稍有不同。
-
getInternalHooks 在 antd Form 暴露的类型定义中是没有的,这是 rc-field-form 的 Form 组件和 Field 组件中需要用到的方法,antd 在类型中把它隐藏了,但实际上还是存在,只是我们日常开发中不应该去使用这个方法。
-
scrollToField 方法没有,这个方法是 antd 实现的。
还有其他一些属性和方法
interface Store {
[name: string]: any;
}
interface Callbacks<Values = any> {
onValuesChange?: (changedValues: any, values: Values) => void;
onFieldsChange?: (changedFields: FieldData[], allFields: FieldData[]) => void;
onFinish?: (values: Values) => void;
onFinishFailed?: (errorInfo: ValidateErrorEntity<Values>) => void;
}
type InternalNamePath = (string | number)[];
class FormStore {
private store: Store = {}; // 存放表单中所有的数据
private initialValues: Store = {}; // 表单初始值,resetFields 时使用此值
private fieldEntities: FieldEntity[] = []; // 保存表单中所有 Field 组件实例
private callbacks: Callbacks = {}; // 回调函数
private setInitialValues = (initialValues: any, init: boolean) => {
// 设置初始值
}
// =========================== Observer ===========================
private registerField = (entity: FieldEntity) => {
// 注册 Field 组件实例,触发 fieldEntities 压栈
}
private dispatch = (action: ReducerAction) => {
// Field 组件在需要修改表单值或触发校验时,通过 dispatch 一个 action,通知 FormStore 处理
}
private notifyObservers = (
prevStore: Store, // 原 Store
namePathList: InternalNamePath[] | null, // 有变更的表单的路径集合
) => {
// 用于通知 Field 组件 Store 有变化,并传入原来的 Store 和新的 Store
}
}
相信大家已经对 FormStore 有了一个大致的认识,它掌管着表单的所有状态,是表单的核心枢纽。
Form
Form 主要做下面这几件事
- 初始化 Form 实例
- 注册回调函数,onFieldsChange、onValuesChange、onFinish
- 设置初始值(仅当第一次渲染时会设置 store,其他情况下仅设置 initialValues)
- 提供 Provider
- 渲染子节点
const Form = ({
form,
initialValues
}: FormProps) => {
// 初始化 Form 实例
const [formInstance] = useForm(form);
const {
useSubscribe,
setInitialValues,
setCallbacks,
setValidateMessages,
setPreserve,
} = (formInstance as InternalFormInstance).getInternalHooks(HOOK_MARK);
// 配置回调函数
setCallbacks({
onValuesChange,
onFieldsChange: (changedFields: FieldData[], ...rest) => {
formContext.triggerFormChange(name, changedFields);
if (onFieldsChange) {
onFieldsChange(changedFields, ...rest);
}
},
onFinish: (values: Store) => {
formContext.triggerFormFinish(name, values);
if (onFinish) {
onFinish(values);
}
},
onFinishFailed,
});
// 标志位,是否第一次挂载
const mountRef = React.useRef(null);
// 设置初始值
setInitialValues(initialValues, !mountRef.current);
// 标志位赋值
if (!mountRef.current) {
mountRef.current = true;
}
// 初始化 context value
const formContextValue = React.useMemo(
() => ({
...(formInstance as InternalFormInstance),
validateTrigger,
}),
[formInstance, validateTrigger],
);
// 提供 Provider
const wrapperNode = (
<FieldContext.Provider value={formContextValue}>{childrenNode}</FieldContext.Provider>
);
return (
<Component
{...restProps}
onSubmit={(event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault();
event.stopPropagation();
formInstance.submit();
}}
>
{wrapperNode}
</Component>
);
}
Field
Field 组件主要有以下几个工作
- 通过 React Context 获取 Form 实例
- 向 FormStore 注册自身
- 向子组件注入 value 和 onChange(value 通过 Form 实例从 FormStore 中获取)
- 渲染子组件
class Field extends React.Component {
public componentDidMount() {
const { shouldUpdate } = this.props;
// 获取 getInternalHooks 方法
const { getInternalHooks }: InternalFormInstance = this.context;
const { registerField } = getInternalHooks(HOOK_MARK);
// 向 FormStore 注册自身
this.cancelRegisterFunc = registerField(this);
}
public onStoreChange() {
// 当 FormStore 里的值变化时(可能是 onChange 触发,也可能是 setFieldsValue 触发)
// FormStore 会调用此函数(Field 已经把自身注册到 FormStore 中)
// 在这里 Field 就会根据一些条件决定是否要进行更新
}
public getControlled() {
const { getInternalHooks, getFieldsValue }: InternalFormInstance = this.context;
const { dispatch } = getInternalHooks(HOOK_MARK);
...
// trigger 默认为 onChange,可以通过 props 更改
control[trigger] = (...args: EventArgs) => {
// Mark as touched
this.touched = true;
this.dirty = true;
let newValue: StoreValue;
if (getValueFromEvent) {
newValue = getValueFromEvent(...args);
} else {
newValue = defaultGetValueFromEvent(valuePropName, ...args);
}
if (normalize) {
newValue = normalize(newValue, value, getFieldsValue(true));
}
// 通知 FormStore 有值变更
dispatch({
type: 'updateValue',
namePath,
value: newValue,
});
if (originTriggerFunc) {
originTriggerFunc(...args);
}
};
...
return control;
}
public render() {
const { resetCount } = this.state;
const { children } = this.props;
// 判断 children 是什么类型
const { child, isFunction } = this.getOnlyChild(children);
let returnChildNode: React.ReactNode;
if (isFunction) {
returnChildNode = child;
} else if (React.isValidElement(child)) {
// 通过 cloneElement 注入 value 和 onChange
returnChildNode = React.cloneElement(
child as React.ReactElement,
this.getControlled((child as React.ReactElement).props),
);
} else {
warning(!child, '`children` of Field is not validate ReactElement.');
returnChildNode = child;
}
return <React.Fragment key={resetCount}>{returnChildNode}</React.Fragment>;
}
}
结语
rc-field-form 通过 React Context 把 FormStore、Form、Field 结合在一起,实现了一种类似状态管理库的机制,可以看成 Form 组件内置了一个状态管理,而 Field 组件则 connect 到了此状态管理库。对使用者来说,这一切都是透明的。
上述是 rc-field-form 的基本思想,有很多细节没有详细剖析,比如 FormList 是如何实现的,依赖校验是怎么回事,性能如何保证等等。有机会再写下来与大家探讨。