1. 前言
读完本文你会收获到:
- Form 原理 及最佳实践,提高日常开发效率。
- Form 源码中运用了哪些设计模式,丰富自己编码的武器库。
2. 原理
一句话概括:数据层面,单个实例统一管理所有表单数据,通过事件订阅和通知机制进行更新与响应,更新时触发相应组件重新渲染。
2.1. Form与Item
2.1.1. 一个实例
每一个 Form 组件都有相应的实例对象,集中管理表单状态、校验规则等。大家最常看到的应该就是如下函数:
const [form] = Form.useForm()
这个函数的最基础的作用是 创建表单实例,实例中包含状态仓库以及对状态的增删查改的方法,以及事件的订阅与通知。
2.1.2. 订阅与通知
看两段简化的代码:
js
private fieldEntities = [];
// 将Item实例存在Form实例中
private registerField = (entity) => {
// 存储子项实例
this.fieldEntities.push(entity);
...
}
// 通知Item
private notifyObservers = (
...
) => {
...
this.getFieldEntities().forEach(({ onStoreChange }) => {
onStoreChange(...);
});
...
};
js
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实例 的 registerField 方法把自身给注册进去?
Item 是 Form 的子组件,在 Form 组件中通过 Context 的方法将 form 实例注入到任何层级的子组件中。所以在 Item 组件中,因为是在 Form 的包裹中,所以自然可以通过 useContext 拿到 form 实例,从而使用 registerField 将自身注册到 form 实例中。
- form实例中统一管理的数据如何与Item中的受控组件进行数据传递的呢?
下一节,Item与受控组件。
2.2. Item与受控组件
我们自定义受控组件或是使用通用组件,组件的 props 一般都会尽量遵循:
const { value, onChange, ... } = props
获取传入的 value 渲染组件,通过 onChange 回调函数的方式将组件变更的数据传递到上层使用。
我们来看Item组件中如何传递 value 和消费 onChange 回调的。
2.2.1. value
value比较简单,只需要Item组件中通过form实例中的方法(见上文),在数据仓库中查找对应字段的值即可。
ini
public getValue = (...) => {
const { getFieldsValue }: FormInstance = this.props.context;
const namePath = this.getNamePath();
...
};
2.2.2. onChange
还是在Item组件中,通过form实例中的方法 dispatch,将更新的值传递到form实例中,我们一起看一下 dispatch 方法:
js
//使用
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中完成闭环。
3. 源码中的设计模式
3.1. 观察者模式
3.1.1. 源码案例(见[2.1.2. 订阅与通知](#3.1.1. 源码案例(见2.1.2. 订阅与通知) "#h212"))
3.1.2. 解析
观察者模式提供了一种对象间一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都得到通知并自动更新。
结合观察者模式的特点,我们在日常的编码中:
- 目前很多复杂的后台编辑项,经常会有不同项联动的效果,开发者就可以考虑优先使用Form表单自带的观察者模式架构去实现联动。
- 后台的表单只是观察者模式的实践之一,结合观察者模式 的抽象概念,有很多业务场景的功能可以考虑使用这种架构去组织代码,例如:购物车场景 。有多个组件依赖数据仓库 (购物车),数据仓库的变更会通知 各组件。大多数的真实场景购物车的初始数据可能是由后端下发的,其实也就是一次修改数据仓库的行为,包括用户的交互。而用户的交互等行为修改数据仓库后,统一通知各订阅单位 ,包括上传购物车数据至后端,也可以看作是后端的对数据仓库的订阅。
在不同的场景中,观察者模式帮助实现了行为实体间的解耦,使得一个实体的变化可以通知到其他关联的实体。
3.2. 单例模式
3.2.1. 源码案例
ini
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];
}
3.2.2. 解析
上面的代码中通过 useForm hook,确保表单状态的唯一性,以避免不同实例之间的状态冲突。
单例模式和观察者模式在管理全局状态、资源、事件等比较适合在一起使用,两个模式的概念能够比较好的融合,观察者模式一对多的对象关系,那中心数据仓库就可以使用单例模式管理起来,提供了一种可维护和解耦的方式。
4. 总结
在中后台的业务开发中,表单必不可少,合理的借用表单组件的设计模式来组织自己的代码结构是我们需要上的第一课。
脱离表单原理的视角,抽象后的设计模式,能让我们在未来的开发中,又多了一件趁手的武器
后续的这个中后台系列的文章都会在为大家讲解原理的基础 上,深入考究在设计模式/代码结构上有哪些值得借鉴学习的地方。
还有哪些模块 值得我们一起学习讨论 欢迎在评论区中留言
长话短说,只讲干货,我们下期再见!
最后
📚 小茗文章推荐:
关注公众号「Goodme前端团队」,获取更多干货实践,欢迎交流分享~