读完本文你会收获到:
一句话概括: 数据层面,单个实例统一管理所有表单数据,通过事件订阅和通知机制进行更新与响应,更新时触发相应组件重新渲染。
每一个 Form 组件都有相应的实例对象,集中管理表单状态、校验规则等。大家最常看到的应该就是如下函数:
const [form] = Form.useForm()
这个函数的最基础的作用是 创建表单实例,实例中包含状态仓库以及对状态的增删查改的方法,以及事件的订阅与通知。
看两段简化的代码:
private fieldEntities = [];
// 将Item实例存在Form实例中
private registerField = (entity) => {
// 存储子项实例
this.fieldEntities.push(entity);
...
}
// 通知Item
private notifyObservers = (
...
) => {
...
this.getFieldEntities().forEach(({ onStoreChange }) => {
onStoreChange(...);
});
...
};
class Field extends ... {
public componentDidMount() {
...
// Item将实例注册到form实例中
this.cancelRegisterFunc = registerField(this);
...
}
// 提供给form实例发送通知的回调函数
public onStoreChange = (prevStore, namePathList, info) => {
...
switch (info.type) {
case 'reset':
...
case 'remove':
...
case 'setField':
this.reRender();
...
case 'dependenciesUpdate':
...
default:
...
break;
}
...
};
// 更新class类型的Item组件
public reRender() {
...
this.forceUpdate();
}
}
从上面的两段简化后的代码可以清晰的看出:
在form实例中一旦有状态的变更只需要遍历Item实例的 onStoreChange ,就可以触发 Item 组件的 update。
而form实例中调用的 onStoreChange 方法实际上是在使用 form实例 的 registerField 注册item实例后,item实例中的方法
但是还是有几个细节需要交代一下:
Item 是 Form 的子组件,在 Form 组件中通过 Context 的方法将 form 实例注入到任何层级的子组件中。所以在 Item 组件中,因为是在 Form 的包裹中,所以自然可以通过 useContext 拿到 form 实例,从而使用 registerField 将自身注册到 form 实例中。
下一节,Item与受控组件。
我们自定义受控组件或是使用通用组件,组件的 props 一般都会尽量遵循:
const { value, onChange, ... } = props
获取传入的 value 渲染组件,通过 onChange 回调函数的方式将组件变更的数据传递到上层使用。
我们来看Item组件中如何传递 value 和消费 onChange 回调的。
value比较简单,只需要Item组件中通过form实例中的方法(见上文),在数据仓库中查找对应字段的值即可。
public getValue = (...) => {
const { getFieldsValue }: FormInstance = this.props.context;
const namePath = this.getNamePath();
...
};
还是在Item组件中,通过form实例中的方法 dispatch,将更新的值传递到form实例中,我们一起看一下 dispatch 方法:
//使用
onChange = (...args) => {
...
dispatch({
type: 'updateValue',
namePath,
value: newValue,
});
...
}
private dispatch = (action) => {
switch (action.type) {
case 'updateValue': {
const { namePath, value } = action;
this.updateValue(namePath, value);
break;
}
...
default:
// Currently we don't have other action. Do nothing.
}
};
private updateValue = (name: NamePath, value: StoreValue) => {
const namePath = getNamePath(name);
const prevStore = this.store;
this.updateStore(setValue(this.store, namePath, value));
this.notifyObservers(prevStore, [namePath], {
type: 'valueUpdate',
source: 'internal',
});
...
};
可以看到当某一个Item的值发生改变时,首先会更新form实例的数据仓库,然后看看有没有字段依赖了当前更新的字段,再通知各订阅了消息的Item实例,最后再将 value 和 onChange 注入到子组件的props中完成闭环。
观察者模式提供了一种对象间一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都得到通知并自动更新。
结合观察者模式的特点,我们在日常的编码中:
在不同的场景中,观察者模式帮助实现了行为实体间的解耦,使得一个实体的变化可以通知到其他关联的实体。
function useForm<Values = any>(form?: FormInstance<Values>): [FormInstance<Values>] {
const formRef = React.useRef<FormInstance>();
...
if (!formRef.current) {
if (form) {
formRef.current = form;
} else {
...
const formStore: FormStore = new FormStore(forceReRender);
formRef.current = formStore.getForm();
}
}
...
return [formRef.current];
}
上面的代码中通过 useForm hook,确保表单状态的唯一性,以避免不同实例之间的状态冲突。
单例模式和观察者模式在管理全局状态、资源、事件等比较适合在一起使用,两个模式的概念能够比较好的融合,观察者模式一对多的对象关系,那中心数据仓库就可以使用单例模式管理起来,提供了一种可维护和解耦的方式。
在中后台的业务开发中,表单必不可少,合理的借用表单组件的设计模式来组织自己的代码结构是我们需要上的第一课。
脱离表单原理的视角,抽象后的设计模式,能让我们在未来的开发中,又多了一件趁手的武器~
后续的这个中后台系列的文章都会在为大家讲解原理的基础上,深入考究在设计模式/代码结构上有哪些值得借鉴学习的地方。
还有哪些模块值得我们一起学习讨论欢迎在评论区中留言~
长话短说,只讲干货,我们下期再见!
📚 小茗文章推荐:
关注公众号「Goodme前端团队」,获取更多干货实践,欢迎交流分享~