在以 React 作为技术栈的中后台系统中,Ant Design 组件库的使用频率还是很高的。而 Form 组件,是 Ant Design 中设计复杂且比较常用的一个组件。Antd4及以上的版本对 Form 组件做了很多优化,在性能方面有很大提升。因为 antd Form 底层依赖 rc-field-form,所以本文主要讲的其实是 rc-field-form 的实现原理。
先来看个例子:
<Form name="basic">
<Form.Item label="Username" name="username">
<Input />
</Form.Item>
</Form>
上面的代码是Antd Form的常规用法,如下图所示Input组件除了被Form、Form.Item组件包裹外,还有两层对外不可见的嵌套,每一层都各司其职。

一、如何创建 Form
const [form] = Form.useForm();
在项目中,通常通过挂载在 Form 上的 useForm hook 来获取 form。
function useForm(form) {
const formRef = React.useRef();
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);
formRef.current = formStore.getForm();
}
}
return [formRef.current];
}
useForm的源码比较简单,通过 new FormStore 得到 formStore 实例化对象,然后执行 formStore.getForm 并赋值给 formRef.current,最后 return [formRef.current],也就是返回我们所需要的 form。
AntdForm 底层最核心的逻辑主要在 FormStore 类中,先看下 FormStore 的部分源码:
class FormStore {
private store = {};
private fieldEntities = [];
// 向外暴露一些私有方法 getFieldsValue、setFieldsValue等
public getForm = (): InternalFormInstance => ({
getFieldValue: this.getFieldValue,
getFieldsValue: this.getFieldsValue,
resetFields: this.resetFields,
setFields: this.setFields,
setFieldValue: this.setFieldValue,
setFieldsValue: this.setFieldsValue,
_init: true,
getInternalHooks: this.getInternalHooks,
});
private registerField = (entity) => {
this.fieldEntities.push(entity);
return () => {
// un-register
}
}
private registerWatch = () => { }
private notifyWatch = () => { }
// ... ...
}
Antd 4 相比 Antd 3,在Form的性能上做了很大提升。Antd 3Form 底层依赖 rc-form,rc-form 主要通过 React state 来存储数据,当有一个表单项数据发生变化时,其他表单组件也会重新 render。而 Antd 4Form 底层依赖的 rc-field-form,则是通过 FormStore 类中的私有静态属性 store 和 fieldEntities 来收集数据和字段信息,如果只是改变静态属性的 value,是无法更新视图的。所以在收集数据的同时,通过发布订阅模式 + (forceRender || updateState),实现数据通信和增量更新视图。除了增量更新视图,使用静态属性而不是React state另一个好处,是给静态属性赋值后,可以同步拿到最新值,不用关注React state异步更新可能导致的意外问题。
二、如何收集字段
在 Field 组件挂载后,registerField(同 FormStore 类中的 registerField)注册,注意这里的入参 this,代表的是 Field 类组件,这里注册整个类组件而不是表单字段,主要是为了在运行时更方便地去获取上下文信息(类组件 props 和类组件内的方法等)。
class Field extends React.Component {
public componentDidMount() {
const { fieldContext } = this.props;
// Register on init
if (fieldContext) {
const { getInternalHooks }: InternalFormInstance = fieldContext;
const { registerField } = getInternalHooks(HOOK_MARK);
this.cancelRegisterFunc = registerField(this);
}
}
public componentWillUnmount() {
this.cancelRegister();
}
}
三、数据如何通信
1、update State
上文说到,rc-field-form 通过 FormStore 中的静态属性 store 来收集数据,并通过发布订阅模式 + (forceRender || updateState) 来实现数据的通信。此处展开来看一下。
const userName = Form.useWatch('userName', form);
在 useWatch 内部,通过 registerWatch 来添加订阅,触发时机是当 store 发生变化时。在 callback 函数内部,会比较 value 是否发生变化,如果变化,则会更新 state,进而触发使用useWatch hook 的组件重新 render。
function useWatch(...args) {
const [value, setValue] = useState<any>();
const valueStr = useMemo(() => stringify(value), [value]);
const valueStrRef = useRef(valueStr);
valueStrRef.current = valueStr;
const isValidForm = formInstance && formInstance._init;
useEffect(() => {
// Skip if not exist form instance
if (!isValidForm) return;
const { getInternalHooks } = formInstance;
const { registerWatch } = getInternalHooks(HOOK_MARK);
// 添加订阅
const cancelRegister = registerWatch((values, allValues) => {
const newValue = getWatchValue(values, allValues);
const nextValueStr = stringify(newValue);
if (valueStrRef.current !== nextValueStr) { // 当监听的字段value发生变化时 update state
valueStrRef.current = nextValueStr;
setValue(newValue);
}
});
return cancelRegister;
}, [isValidForm]);
return value;
}
class FormStore {
private watchList = [];
private registerWatch = callback => {
this.watchList.push(callback);
return () => {
this.watchList = this.watchList.filter(fn => fn !== callback);
};
};
private notifyWatch = (namePath) => {
if (this.watchList.length) {
const values = this.getFieldsValue(); // 不包含被删除字段的值
const allValues = this.getFieldsValue(true); // 包含被删除字段的值
this.watchList.forEach(callback => {
// 批量执行callback
callback(values, allValues, namePath);
});
}
};
private notifyObservers = (
prevStore,
namePathList,
info,
) => {
const mergedInfo = {
...info,
store: this.getFieldsValue(true),
};
this.getFieldEntities().forEach(({ onStoreChange }) => {
onStoreChange(prevStore, namePathList, mergedInfo);
});
};
private updateValue = (name, value) => {
const namePath = getNamePath(name);
const prevStore = this.store;
this.updateStore(setValue(this.store, namePath, value));
this.notifyObservers(prevStore, [namePath], {
type: 'valueUpdate',
source: 'internal',
});
// this.store更新时,通知订阅者
this.notifyWatch([namePath]);
};
}
2、forceRender
rc-field-form 在收集字段的时候,registerField(this) 的入参 this 指的是 Field 类组件,所以当 store 改变时,notifyObservers 方法中的 onStoreChange(Field 组件中的 onStoreChange),在 onStoreChange 中,会比较新旧 value 是否一致,如果不一致,则会通过 forceUpdate 强制更新组件。通过 forceUpdate 更新组件的好处是不用再去维护一个 state,在自定义 hook 中,则只能通过更新 state 来实现组件的重新 render。
看到这里,你可能会疑问,通过forceUpdate更新组件的目的是什么呢?主要是为了向表单组件注入value。因为store只是FormStore的一个静态属性,store的变化不会触发组件的重新渲染,所以需要通过forceUpdate更新Field组件,从而向表单组件注入新的value。我们来看下getControlled方法,getControlled对表单组件的props进行了劫持并返回新的props,然后通过React.cloneElement传递给表单组件。
class Field extends React.Component {
public reRender() {
this.forceUpdate();
}
// Trigger by store update. Check if need update the component
public onStoreChange = (prevStore, namePathList, info) => {
const { store } = info;
const prevValue = this.getValue(prevStore);
const curValue = this.getValue(store);
if (prevValue !== curValue) {
this.reRender();
}
};
public getControlled = childProps => {
const {
trigger = 'onChange',
validateTrigger,
getValueFromEvent,
normalize,
valuePropName,
getValueProps,
fieldContext,
} = this.props;
const mergedValidateTrigger = validateTrigger || fieldContext.validateTrigger;
const namePath = this.getNamePath();
const { getInternalHooks, getFieldsValue } = fieldContext;
const { dispatch } = getInternalHooks(HOOK_MARK);
const value = this.getValue();
const mergedGetValueProps = getValueProps || (val => ({ [valuePropName]: val }));
const originTriggerFunc = childProps[trigger];
const control = {
...childProps,
...mergedGetValueProps(value), // mergedGetValueProps的返回值包含value属性
};
// Add trigger
control[trigger] = e => {
let newValue;
if (getValueFromEvent) {
newValue = getValueFromEvent(e);
} else {
newValue = defaultGetValueFromEvent(valuePropName, e);
}
dispatch({
type: 'updateValue',
namePath,
value: newValue,
});
if (originTriggerFunc) {
originTriggerFunc(e);
}
};
return control;
};
public render() {
const { children } = this.props;
const { child } = this.getOnlyChild(children);
const returnChildNode = React.cloneElement(
child as React.ReactElement,
this.getControlled(child.props),
);
return <React.Fragment>{returnChildNode}</React.Fragment>;
}
}
下面这段代码,是不是在我们的代码中偶尔会出现,你有没有好奇过,当我们手动往表单组件传入 onChange 时,我们传入的 onChange 可以正常执行,当我们输入内容时,数据也可以被正常收集,怎么做到的呢?
其实是在 getControlled 内部对 onChange 进行了重写,首先通过 originTriggerFunc 暂存我们传入的 onChange,然后在重写的 onChange 内部执行 originTriggerFunc。
const onChange = (e) => {
// doing something
}
<Form.Item label="Username" name="username">
<Input placeholder="Username" onChange={onChange} />
</Form.Item>
四、如何校验数据
Field 内部会向表单组件注入一个回调方法,在底层会遍历rules完成数据校验,触发时机默认为 onChange,我们可以通过传入 validateTrigger 来控制数据的校验时机。数据校验完成后,会调用父组件注入的 onMetaChange 方法,将 meta 数据传递给父组件(Antd Form.Item)。Antd Form.Item 拿到 meta 数据后,通过 update state,将 errors 内容渲染到页面上。
// meta信息
const meta = {
touched: this.isFieldTouched(),
validating: this.prevValidating,
errors: this.errors,
warnings: this.warnings,
name: this.getNamePath(),
validated: this.validatePromise === null,
};
提交表单的时候,如何将数据校验不通过的表单显示到可视区域呢?

上图中,可以看到input有一个id属性,而id是在Form Item组件中动态生成的,结合我们开头的代码可以知道,id的生成规则:form name + 表单 name。当数据校验不通过时,通过id定位到具体dom元素,然后通过scrollIntoView将表单项滚动到可视区域。
const scrollToField=(name, options) => {
const namePath = toArray(name);
const fieldId = getFieldId(namePath, wrapForm.__INTERNAL__.name);
const node = fieldId ? document.getElementById(fieldId) : null;
if (node) {
scrollIntoView(node, {
scrollMode: 'if-needed',
block: 'nearest',
...options,
} );
}
}
源码地址:
Antd:https://github.com/ant-design/ant-design
rc-field-form:https://github.com/react-component/field-form
rc-form(Used by antd3):https://github.com/react-component/form