Antd4的Form表单的使用想必大家不会陌生,随着使用的越多,其背后的原理实现也是我们前端开发者想要了解的,接下来简单实现下表单的源码,使用React Hook + Typescript
Form表单无非做了一下几件事:
- 数据收集
- 数据传递
- 数据响应式
- 表单校验
- 表单提交
- 表单重置
数据收集
Form表单里的input、radio这些数据项需要做成受控组件,传统的做法是放到Form父组件中的state中,其中antd3就是这么做的,但是这种方式有一个缺点就是只要Form中有一个数据项发生了变化,那都要执行Form的setState,这就意味着整个Form表单都要更新,这不合理。
于是乎,类似于redux这种数据仓库的做法就收到了青睐,定义一个单独的数据仓库useForm(自定义Hook)在数据仓库中设置get和set方法暴露出来即可
定义了store对象来存储Field组件中input等数据项形式如[name]:value
定义组件实例数组fieldEntities存取Field组件实例,包括{props, onStoreChange}
// useForm.ts
/**
* 数据仓库-存储form
*/
class FormStore {
// 数据仓库
private store: Store = {};
...
// get
private getFieldsValue = (): Store => {
return this.store;
};
private getFieldValue = (name: string): any => {
return this.store[name];
};
// set
private setFieldsValue = (newStore: Store): void => {
// 'name': 'value'
// 第一步:更新数据仓库
this.store = Object.assign({}, this.store, newStore);
// 第二步:更新组件-forceUpdate
this.fieldEntities.forEach(field => {
// 更新对应name上的field,而不是每次都全部更新
Object.keys(newStore).forEach(k => {
if (k === field.props.name) {
field.onStoreChange();
}
});
});
};
...
// 提交
private submit = () => {
// 校验成功onFinish
// 校验失败onFinishFailed
};
// 给用户暴露的API
public getForm = (): FormInstance => ({
getFieldValue: this.getFieldValue,
getFieldsValue: this.getFieldsValue,
setFieldsValue: this.setFieldsValue,
submit: this.submit
});
}
...
数据仓库创建好后,需要存储实例,需要注意的是组件会发生更新,要确保组件更新和初次渲染的时候使用的是同一个数据仓库实例,则需要使用useRef来保证在组件整个生命周期间保持不变
// useForm.ts
...
function useForm<Values = any>(
form?: FormInstance<Values>
): [FormInstance<Values>] {
// 保证始终用到同一个对象
const formRef = React.useRef<FormInstance>();
if (!formRef.current) {
const formStore: FormStore = new FormStore();
formRef.current = formStore.getForm();
}
return [formRef.current];
}
数据传递
数据仓库创建好后,接下来就是各个组件对数据仓库的访问。考虑到Form组件、input、radio组件等都要访问数据仓库,并且它们有个共同的特点,都是Form的子组件,只是不确定是Form的第几代子孙组件,这个时候使用props显然不合适,所以需要React提供的context方法跨层级传递对象,分为三步走:
1、创建context对象
// FieldContext.ts
import React from "react";
const FieldContext = React.createContext<FormInstance>();
export default FieldContext;
2、使用Provider传递value
// Form.tsx
...
import FieldContext from "./Context";
import useForm from "./useForm";
export default function Form({children}){
const [formInstance] = useForm()
<form onSubmit={onSubmit}>
<FieldContext.Provider value={formInstance}>
{children}
</FieldContext.Provider>
</form>
}
3、子组件消费value,函数组件中使用useContext(context) Hook方法
// Field.tsx
// 获取context对象
const fieldContext = useContext(FieldContext);
// 额外的属性-input
function getControlled() {
const { getFieldValue, setFieldsValue } = fieldContext;
return {
value: getFieldValue(name), // get
onChange: (e: any) => {
// set
const newValue = e.target.value;
setFieldsValue({ [name]: newValue });
}
};
}
// 克隆组件,给其添加一些属性返回,使Field=>Input变成受控组件
const returnChildNode = React.cloneElement(
children as React.ReactElement,
getControlled()
);
return (
<div style={{ position: "relative" }}>
<div style={{ display: "flex", alignItems: "center" }}>
<label style={{ flex: "none", width: 50 }}>{label || name} </label>
{returnChildNode}
</div>
</div>
);
数据响应式
用Field包裹的input、radio等数据项缺少数据响应式,即value和onChange事件,另外store中的数据发生改变,Field组件也应该随之更新,而React中更新组件有四种方式:ReactDOM.render、forceUpdate、setState或者父组件更新,这里应该用forceUpdate
那么现在其实要做的就是加上注册组件更新,监听this.store,一旦this.store中的某个值改变,就更新对应的组件。
首先在FormStore中加上存储Form中子组件实例的方法
// useForm.ts
class FormStore {
// 数据仓库
private store: Store = {};
// 组件实例数组
private fieldEntities: FieldEntity[] = [];
...
// 存取组件实例
private setFieldEntities = (field: FieldEntity) => {
this.fieldEntities.push(field);
return () => {
// 取消注册
this.fieldEntities = this.fieldEntities.filter(f => f !== field);
// 删除数据仓库里的数据
delete this.store[field.props.name];
};
};
// get
private getFieldsValue = (): Store => {
return this.store;
};
private getFieldValue = (name: string): any => {
return this.store[name];
};
// set
private setFieldsValue = (newStore: Store): void => {
// 'name': 'value'
// 第一步:更新数据仓库
this.store = Object.assign({}, this.store, newStore);
// 第二步:更新组件-forceUpdate
this.fieldEntities.forEach(field => {
// 更新对应name上的field,而不是每次都全部更新
Object.keys(newStore).forEach(k => {
if (k === field.props.name) {
field.onStoreChange();
}
});
});
};
....
}
接下来在Field组件中执行注册和取消注册
const Field: React.FC<FieldProps> = (props: FieldProps) => {
const { children, label, name } = props;
// 获取context对象
const fieldContext = useContext(FieldContext);
const [, forceUpdate] = useReducer(x => x + 1, 0);
// 使用useEffect可能会使订阅延迟
useLayoutEffect(() => {
// 函数组件没有实例对象this,把props和onStoreChange当做对象传过去
const unRegister: any = fieldContext.setFieldEntities({
props,
onStoreChange
});
// 组件销毁取消订阅
return () => {
unRegister();
};
}, [props, fieldContext]);
// 强制更新组件
function onStoreChange() {
forceUpdate();
}
....
};
表单检验
提交前,首先要做表单校验。校验通过,则执行onFinish,失败则执行onFinishFailed,表单校验的依据是Field的rules,这里做的简单的校验,只要不是null、undefined或者空字符串,就当校验通过,否则的话,校验不通过,并且err数组push错误信息
// useForm.ts
// 校验
private validate = () => {
let err: object[] = [];
// 实现基础功能,如输入信息就通过
this.fieldEntities.forEach(field => {
const { name, rules } = field.props;
let rule = rules && rules[0];
let value = this.getFieldValue(name);
if (
rule &&
rule.required &&
(value == undefined || value.replace(/\s*/, "") === "")
) {
err.push({ name, err: rule.message });
}
});
return err;
};
表单提交
完成表单校验后,就是表单提交方法,即onFinish和onFinishFailed方法,由于这两个是Form组件的props参数,所以可以在FormStore中定义this.callbacks存储
// useForm.ts
class FormStore{
// 保存成功和失败回调函数
private callbacks: Callbacks = {};
...
// 存取回调函数-成功或失败
private setCallbacks = (callbacks: Callbacks) => {
this.callbacks = callbacks;
};
}
接下李在Form中执行setCallbacks即可
// Form.tsx
// 存取回调函数-成功和失败
formInstance.setCallbacks({ onFinish, onFinishFailed });
表单重置
给表单设置初始值,即Field的属性initialValue,要保证我们使用的数据仓库在组件的任何生命周期都是同一个,即初始化一次,后续在这个基础上更新,给Form的props加个参数form
// Form.tsx
export default function Form({..., form}){
const [formInstance] = useForm(form)
}
修改后的useForm如下:
// useForm.ts
...
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();
formRef.current = formStore.getForm();
}
}
return [formRef.current];
}
设置初始值和重置方法在FormStore中
// useForm.ts
class FormStore{
...
// 初始值
private initialValues: Store = {};
// 存取初始值
private setInitialValues = (initialStore: Store): void => {
this.initialValues = Object.assign({}, this.initialValues, initialStore);
};
// 重置
private resetFields = (): void => {
this.setFieldsValue(this.initialValues);
};
}
至此,一个简单的Form表单就差不多了,代码地址github地址