以antd5.0为例, antd表单的实现主要关注"rc-field-form"
先看代码(主要看Form、useForm、Field)
初始化表单组件,实例化表单构造函数, 为Field组件提供注册方法以及初始化方法等
// src/Form.tsx(仓库简化代码示例)
// 创建context
const formContext: FormContextProps = React.useContext(FormContext);
// 通过useForm中的FormStore构造函数构造表单实例
const [formInstance] = useForm(form);
// useForm.tsx (https://github.com/react-component/field-form/blob/master/src/useForm.ts)
function useForm() {
const formRef = React.useRef<FormInstance>();
const formStore: FormStore = new FormStore(forceReRender);
formRef.current = formStore.getForm();
}
}
return [formRef.current];
}
字段Field的注册
//src/Field.tsx
// rende方法
public render() {
returnChildNode = React.cloneElement(
child as React.ReactElement,
// 重新组合组件props(注入
this.getControlled((child as React.ReactElement).props),
);
return <React.Fragment key={resetCount}>{returnChildNode}</React.Fragment>;
}
}
// getControlled 处理表单项组件props
public getControlled = (childProps: ChildProps = {}) => {
// 通过FieldContext获取表单实例的dispatch方法
const { getInternalHooks, getFieldsValue }: InternalFormInstance = fieldContext;
const { dispatch } = getInternalHooks(HOOK_MARK);
const control = {
...childProps,
...mergedGetValueProps(value),
};
// Add trigger
control[trigger] = (...args: EventArgs) => {
// Mark as touched
this.touched = true;
this.dirty = true;
this.triggerMetaEvent();
let newValue: StoreValue;
if (getValueFromEvent) {
newValue = getValueFromEvent(...args);
} else {
newValue = defaultGetValueFromEvent(valuePropName, ...args);
}
if (normalize) {
newValue = normalize(newValue, value, getFieldsValue(true));
}
// 设置表单项的值
dispatch({
type: 'updateValue',
namePath,
value: newValue,
});
if (originTriggerFunc) {
originTriggerFunc(...args);
}
};
// Add validateTrigger
const validateTriggerList: string[] = toArray(mergedValidateTrigger || []);
validateTriggerList.forEach((triggerName: string) => {
// Wrap additional function of component, so that we can get latest value from store
const originTrigger = control[triggerName];
control[triggerName] = (...args: EventArgs) => {
if (originTrigger) {
originTrigger(...args);
}
// Always use latest rules
const { rules } = this.props;
if (rules && rules.length) {
// We dispatch validate to root,
// since it will update related data with other field with same name
dispatch({
type: 'validateField',
namePath,
triggerName,
});
}
};
});
return control;
};
store状态广播:
当调用setFieldsValue时、validateFields时等会进行状态广播来通知对应的Field,然后由Filed决定是否要更新(见下文)
getControlled通过FieldContext获取表单实例的dispatch方法来更新表单值, 具体updateValue方法实现在useForm中,如下所示,通过调用notifyObservers方法获取每个Field实例,这个实例是在componentDidMount生命周期内调用form实例的registerField进行注册的
// Field.tsx
public componentDidMount() {
// Register on init
if (fieldContext) {
const { getInternalHooks }: InternalFormInstance = fieldContext;
const { registerField } = getInternalHooks(HOOK_MARK);
// 通过registerField注册
this.cancelRegisterFunc = registerField(this);
}
}
通过notifyObservers方法进行广播当前state
// useForm.tsx
private notifyObservers = (
prevStore: Store,
namePathList: InternalNamePath[] | null,
info: NotifyInfo,
) => {
if (this.subscribable) {
const mergedInfo: ValuedNotifyInfo = {
...info,
store: this.getFieldsValue(true),
};
this.getFieldEntities().forEach(({ onStoreChange }) => {
onStoreChange(prevStore, namePathList, mergedInfo);
});
} else {
this.forceRootUpdate();
}
};
Field组件的更新逻辑判定
上述是通过调用所有注册的Flied实例进行广播的,组件的渲染不能没有更新的也要去刷新,所以需要有一层控制,这层控制是在Fild组件层统一做了处理,通过组件的namePathMatch路径匹配进行判断实现精准更新Field状态
// src/Field.tsx
// 被上述 useForm.tsx 里的 notifyObservers 方法所调用,并检查是否需要更新,检查条件有fied name、等等
public onStoreChange: FieldEntity['onStoreChange'] = (prevStore, namePathList, info) => {
if (
namePathMatch ||
((!dependencies.length || namePath.length || shouldUpdate) &&
requireUpdate(shouldUpdate, prevStore, store, prevValue, curValue, info))
) {
this.reRender();
return;
}
}
// 通过notifyObservers广播给每个Field组件,由对应Field通过namtPath、依赖等判断决定是否要重新渲染(forceUpdate)
private updateValue = (name: NamePath, value: StoreValue) => {
this.notifyObservers(prevStore, [namePath], {
type: 'valueUpdate',
source: 'internal',
});
};
小结
从以上的一个简要代码逻辑看整个表单的设计是通过手动的forceupdate来进行手动控制决定是否要渲染,这个判定是通过值的更新(路径判定)、依赖更新、depencencies脏检查、以及shouupdate脏检查来判断是否要更新,目前用下来是没有发现有额外的更新开销。
以上文章是之前用ant3时候表单性能过于卡顿,特别是涉及到一些中后台的复杂表单,转用了formily,后来antd4解决了不能精确渲染的卡顿问题,之前fomily的上提了个discuss讨论精确渲染问题,有兴趣可以看下作者说的跨表单联动的性能问题~