介绍
ant-design 表单组件Form的核心实现机制研究
结构图
流程图
知识点列表
如何实现表单数据的变化监听数据的收集 && 修改数据更新UI视图
数据双向绑定的简易实现
- 项目开发中,我们一般通过更新state或者context,或者是context包装成的props(redux)的值触发UI的更新,如果是一个静态变量,则无法进入生命周期对UI的后续视图产生影响。
- 想要修改静态代码后影响UI视图,可以通过代码调用rerender的实现,rerender方法,class组件有forceRender方法,函数组件则可以通过useState或者useReducer实现rerender
- 通过React.cloneElement可以实现value和onChange逻辑的干预
initialValues如何初始化表单数据并显示
有两种使用方式,一种是在Form组件中传入initialValues,一种是在Field组件中传入initialValue
在Form组件中传入initialValues
在FormStore.setInitialValues将initialValues存储在FormStore.store,只会执行一次
// Set initial value, init store value when first mount
const mountRef = React.useRef(null);
setInitialValues(initialValues, !mountRef.current);
if (!mountRef.current) {
mountRef.current = true;
}
父组件Form先渲染,子组件Field再渲染,在Field组件中,在getControlled
中返回劫持后的value,value通过getValue方法从FormStore.store中获取到,此时获取到的值就已是initialValues初始化过的数据,表单得到正确渲染
const value = this.getValue();
const mergedGetValueProps = getValueProps || ((val: StoreValue) => ({ [valuePropName]: val }));
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const originTriggerFunc: any = childProps[trigger];
const control = {
...childProps,
...mergedGetValueProps(value),
};
return control
在Field组件中传入initialValue
在Field组件的constructor中执行initEntityValue
constructor(props: InternalFieldProps) {
super(props);
// Register on init
if (props.fieldContext) {
const { getInternalHooks }: InternalFormInstance = props.fieldContext;
const { initEntityValue } = getInternalHooks(HOOK_MARK);
initEntityValue(this);
}
}
将Field
组件传入的initialValue
取出,初始化FormStore
。这里检验了prevValue
是否存在,如果父组件Form组件有传入initialValues
则会先初始化Store,则此时prevValue就已存在值,Field组件的initialValue将不会执行初始化Store
private initEntityValue = (entity: FieldEntity) => {
const { initialValue } = entity.props;
if (initialValue !== undefined) {
const namePath = entity.getNamePath();
const prevValue = getValue(this.store, namePath);
if (prevValue === undefined) {
this.updateStore(setValue(this.store, namePath, initialValue));
}
}
};
在Form组件setInitialValues时初始化,初始化存储FormStore.initialValues,但是Field上的initialvalue是不会存储到FormStore.initialValues的,在resetFields有其他处理
如何收集通过人机交互表单值发生改变后的值
通过劫持onChange,通过getControlled返回再传递给表单组件。 分两部分逻辑。一是执行originTrigger的逻辑。二是执行校验的逻辑
originTrigger
- 通过
e.target.[valuePropName]
获取组件的值,通过getValueFromEvent
兼容自定义取值方式 - 传入
normalize
对取值做自定义处理 dispatch updateValue
,附带namePath,转换后的valueupdateStore
内部包含以下处理逻辑:FormStore.updateStore
将最新的数据存储到FormStore.storeFormStore.notifyObservers
通知全部的field更新,如果name匹配,则调用reRender,更新该Field视图FormStore.notifyWatch
通知useWatch监听的函数回调FormStore.triggerDependenciesUpdate
通知全部的field更新,如果匹配dependency则更新该Field视图FormStore.onValuesChange
触发字段值更新时触发回调事件onValuesChangeFormStore.onFieldsChange
触发字段更新时触发回调事件onFieldsChange
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调用updateValue,updateValue
dispatch({
type: 'updateValue',
namePath,
value: newValue,
});
if (originTriggerFunc) {
originTriggerFunc(...args);
}
};
执行校验
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,
});
}
如何实现数据的重置
form.resetFields();
this.updateStore(setValues({}, this.initialValues));
使用this.initialValues更新storethis.resetWithFieldInitialValue();
,获取到Field上的initailValue更新store,如果initialValues上已存在初始化值,以Form的initialValues为准this.notifyObservers(prevStore, null, { type: 'reset' });
通知观察者(组件)更新UI,调用Field的onStoreChange中通过resetCount加一更新组件视图this.notifyWatch();
通知watch cb回调
如何实现的onFinish和onFinishFailed
Form组件最终会被渲染成<form>...</form>
,点击type为submit
的按钮会触发组件上的onSubmit执行formInstance.submit();
...
// 默认为form
component: Component = 'form',
...
<Component
{...restProps}
onSubmit={(event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault();
event.stopPropagation();
// 执行表单提交
formInstance.submit();
}}
>
{wrapperNode}
</Component>
FormStore.validateFields()
执行校验FormStore.onFinish()
执行成功回调FormStore.onFinishFailed()
执行失败回调
private submit = () => {
this.warningUnhooked();
// 执行校验
this.validateFields()
.then(values => {
const { onFinish } = this.callbacks;
if (onFinish) {
try {
// 执行onFinish回调
onFinish(values);
} catch (err) {
// Should print error if user `onFinish` callback failed
console.error(err);
}
}
})
.catch(e => {
const { onFinishFailed } = this.callbacks;
if (onFinishFailed) {
// 执行onFinishFailed回调
onFinishFailed(e);
}
});
};
如何实现校验
校验的时机。
- onchange时校验,代码执行
validateFields
。 - 手动提交表单时代码调用
validateFields
核心demo:codesandbox.io/s/form-vali…
dependencies如何实现依赖更新后重新渲染
更新数据后会触发FormStore.triggerDependenciesUpdate
FormStore.triggerDependenciesUpdate
触发依赖更新FormStore.notifyObservers
通知观察者更新UIthis.getFieldEntities()
获取全部Field进行遍历触发其Field.onStoreChange
- 在
Field.onStoreChange
中判断当前Field的dependencies
是否依赖当前更新的组件,如果依赖则reRender
该Field
如何实现的表单销毁后保存数据
可通过form.getFieldsValue(true)获取全部的表单数据,绕过内部过滤逻辑
测试demo: codesandbox.io/s/ce-shi-fr…
Form
组件。在destroyForm
的逻辑中,将需要保存值的name保存在了this.prevWithoutPreserves
中
private destroyForm = () => {
const prevWithoutPreserves = new NameMap<boolean>();
this.getFieldEntities(true).forEach(entity => {
if (!this.isMergedPreserve(entity.isPreserve())) {
prevWithoutPreserves.set(entity.getNamePath(), true);
}
});
this.prevWithoutPreserves = prevWithoutPreserves;
};
Field
组件。registerField
返回的cancelRegisterFunc
中逻辑,如果preserve
为true,则不销毁store中的数据。
// un-register field callback
return (isListField?: boolean, preserve?: boolean, subNamePath: InternalNamePath = []) => {
this.fieldEntities = this.fieldEntities.filter(item => item !== entity);
// Clean up store value if not preserve
if (!this.isMergedPreserve(preserve) && (!isListField || subNamePath.length > 1)) {
const defaultValue = isListField ? undefined : this.getInitialValue(namePath);
if (
namePath.length &&
this.getFieldValue(namePath) !== defaultValue &&
this.fieldEntities.every(
field =>
// Only reset when no namePath exist
!matchNamePath(field.getNamePath(), namePath),
)
) {
const prevStore = this.store;
this.updateStore(setValue(prevStore, namePath, defaultValue, true));
// Notify that field is unmount
this.notifyObservers(prevStore, [namePath], { type: 'remove' });
// Dependencies update
this.triggerDependenciesUpdate(prevStore, namePath);
}
}
this.notifyWatch([namePath]);
};
在不刷新页面的情况下,后续再次加载Form
组件时调用setInitialValues
会从原有的store
中获取到保存的数据并再次初始化,卸载组件时保存的数据比initialValues
的优先级高
private setInitialValues = (initialValues: Store, init: boolean) => {
this.initialValues = initialValues || {};
if (init) {
let nextStore = setValues({}, initialValues, this.store);
// We will take consider prev form unmount fields.
// When the field is not `preserve`, we need fill this with initialValues instead of store.
// eslint-disable-next-line array-callback-return
this.prevWithoutPreserves?.map(({ key: namePath }) => {
nextStore = setValue(nextStore, namePath, getValue(initialValues, namePath));
});
this.prevWithoutPreserves = null;
this.updateStore(nextStore);
}
};
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);
formRef.current = formStore.getForm();
}
}
return [formRef.current];
}
useForm()
,传入forceReRender
实例化FormStore
,并将formStore.getForm()
闭包对象存储在formRef
上,返回formRef.current
。formStore.getForm()
闭包对象包含了众多FormStore
内部方法,通过useForm()
获得的ref对象可以很方便的操作FormStore
内部方法。formRef.current
将传递给Form
组件,在Form
组件内部调用const [formInstance] = useForm(form);
,此时formInstance
指向通过useForm()
创建的formRef.current
,是同一个对象
useWatch的作用
在指定name的value发生变化的时候获取到最新的value
const cancelRegister = registerWatch(store => {
const newValue = getValue(store, namePathRef.current);
const nextValueStr = stringify(newValue);
// Compare stringify in case it's nest object
if (valueStrRef.current !== nextValueStr) {
valueStrRef.current = nextValueStr;
setValue(newValue);
}
});
// TODO: We can improve this perf in future
const initialValue = getValue(getFieldsValue(), namePathRef.current);
setValue(initialValue);
- registerWatch() 向FormStore传递callback,存储在FormStore.watchList上
- notifyWatch方法会从FormStore.watchList取出callback,逐一执行。当updateValue、setFieldsValue、resetFields、setFields等方法中有调用notifyWatch
- 获取initialValue值,返回给value返回。执行一次。
name传递一个数组有什么作用
支持嵌套的initialValues 在初始化和更新Store时都是可以按照namePath存储到嵌套属性里面
List组件是如何实现的
核心在于Field组件可以接收一个函数,在List组件的实现中也是内嵌了Field组件,内部传入一个函数可以接受Field组件传入的参数(value、onchange还以一起其他的对象和方法)。然后给children传入目标的field的值和操作方法(增加、删除、移动),这样children必须是一个函数,由用户自定义实现数组表单的渲染逻辑
fields们是何时被FormInstance收集到的
Field组件componentDidMount会通过registerField向FormInstance组件传递自己,这样FormInstance就收集到全部的Field,FormInstance每次使用都是FormStore的唯一单例。
public componentDidMount() {
const { shouldUpdate, fieldContext } = this.props;
this.mounted = true;
// Register on init
if (fieldContext) {
const { getInternalHooks }: InternalFormInstance = fieldContext;
const { registerField } = getInternalHooks(HOOK_MARK);
this.cancelRegisterFunc = registerField(this);
}
// One more render for component in case fields not ready
if (shouldUpdate === true) {
this.reRender();
}
}
setValues的作用
每次更新Store都指向一个全新的对象,支持嵌套合并,归功于setValues的实现
每层进行属性合并
/**
* Copy values into store and return a new values object
* ({ a: 1, b: { c: 2 } }, { a: 4, b: { d: 5 } }) => { a: 4, b: { c: 2, d: 5 } }
*/
function internalSetValues<T>(store: T, values: T): T {
const newStore: T = (Array.isArray(store) ? [...store] : { ...store }) as T;
if (!values) {
return newStore;
}
Object.keys(values).forEach(key => {
const prevValue = newStore[key];
const value = values[key];
// If both are object (but target is not array), we use recursion to set deep value
const recursive = isObject(prevValue) && isObject(value);
newStore[key] = recursive ? internalSetValues(prevValue, value || {}) : cloneDeep(value); // Clone deep for arrays
});
return newStore;
}
深度复制
function cloneDeep(val) {
if (Array.isArray(val)) {
return cloneArrayDeep(val);
} else if (typeof val === 'object' && val !== null) {
return cloneObjectDeep(val);
}
return val;
}
function cloneObjectDeep(val) {
if (Object.getPrototypeOf(val) === Object.prototype) {
const res = {};
for (const key in val) {
res[key] = cloneDeep(val[key]);
}
return res;
}
return val;
}
function cloneArrayDeep(val) {
return val.map(item => cloneDeep(item));
}
export default cloneDeep;
更新机制
总的来说,UI更新有两种场景,一种是修改了自身表单的值需要更新UI,另一种是更新自己的值,然后其他依赖了这个表单的其他表单需要更新UI
- notifyObservers更新发生值改变的Field本身。人机交互触发某个Field的onchange,distach({type: 'updateValue',...})进入到FormStore的方法updateValue,调用notifyObservers,获取到全部的getFieldEntities,循环调用Field的onStoreChange方法,由于info.type为valueUpdate所以走最后一个switch,只有name匹配上且值发生了改变,更新此Field
- triggerDependenciesUpdate。更新依赖值发生了变更的Field的Field
render props
Form组件和Field组件都支持render props,render props方式就是children是一个函数,用于接受Form组件和Field组件的组件状态,并通过函数传递给外部使用的方式。
总结
UI组件源码的阅读理解,总结以下技巧:
- 可从实现功能出发去找核心实现,忽略次要逻辑
- 好的源码库在变量和方法命名上语义化都比较好,可根据语义和注释猜测功能
- 比较冗长的实现不便找到核心逻辑的,可在网络上找找相应文章,能提高效率,不必死磕