最近项目里在作咱们本身的组件库,关于表单这块,如何实现一个更简单的表单方案,是咱们一直在讨论的问题,以前项目里习惯用 ant-design
的 Form 表单,也以为蛮好用的,咱们但愿能作出更简洁的方案。html
下面列出了表单相关的解决方案,React
社区的轮子真是多到没法想象:react
以上的表单方案主要聚焦在一下几点:git
value
和 onChange
,有的表单是增长函数(ant-design
)或容器(FormBinder
,Fusion
等),为子组件注册 value
,onChange
,有的是自定义 Feild
组件(UForm
),内部处理相关逻辑关于表单数据收集,能够参考双向数据绑定,下面是双向数据绑定的讨论:github
以及关于实现双向数据绑定的文章:算法
一个是数据收集,一个是渲染,也就是所谓的双向数据绑定,总结起来有三个途径:redux
Babel
插件ant-design
、ice
等等,确实也都蛮好用的,上面列出的文章均可以研读一下,颇有意义value
和 onChange
,除非你的系统里只有一个表单。。。看了大佬们的实现,咱们也想造个轮子,但愿还能够更简洁,让表单写起来更开心,当系统里有不少表单,都要手绑 value
和 onChange
确定是不行的,即使 ant-design
、ice
等,还要加额外的函数或容器,因此目标就是下面这样:缓存
import {Form,Input} form 'form';
export default class FormDemo extends Component<any, any> {
public state = {
value: {
name: '',
school: '',
},
}
public onFormChange = (value) => {
console.log(value);
this.setState({
value,
});
}
public onFormSubmit = () => {
// console.log('submit')
}
public render() {
const me = this;
const {
value,
} = me.state;
return (
<Form
value={value}
enableDomCache={false}
onChange={me.onFormChange}
onSubmit={me.onFormSubmit}
>
<div className="container">
<input
className="biz-input"
data-name="name"
data-rules={[{ max: 10, message: '最大长度10' }]}
type="text"
/>
<Input
data-name="school"
data-rules={[{ max: 10, message: '最大长度10' }]}
type="text"
/>
<Button type="primary" htmlType="submit">提交</Button>
</div>
</Form>
)
}
}
复制代码
value
、onChange
的组件,好比 ant-design
的表单组件ant-design
设计,使用 async-validator
库来作看得出来,咱们是 ant-design
的粉丝了,坦白说,大佬们的方案已经足够简洁了,ant-design
是先驱,后继者 Ice
, Fusion
等多对标 ant-design
,力图更给出更简洁的方案,他们也确实很简洁,特别是 Fusion
的 Field
组件,眼前一亮的感受,UForm
使用相似 JSON Schema(JSchema)
的语法写表单,Uform
和final-form
强调字段的分布式管理,高性能,不过,这两个方案有必定的学习成本,实现方案天然是复杂的。性能优化
不过,当我说出咱们的实现,你们估计要吐槽,由于咱们的实现太简单(捂脸),简单到怀疑人生。bash
要想实现上面的目标,显然文章开头文章列表已经有人实践了,编译期注入代码,不过你要新加个 Babel
插件,不知道你喜不喜欢。async
咱们的实现是采用运行时修改虚拟DOM
的,不在编译期作,也就是运行时来作了,不过,不会在组件外加额外的函数或容器,只是利用 Form
容器来实现,你们必定想到了,那样是否是要遍历全部子节点?这样会不会有额外的性能开销?
那就先实现,再优化。
首先,须要遍历全部子 虚拟DOM
节点,深度优先,判断节点是否有 data-name
或者 name
属性,若是有,为该组件附加 value
和 onChange
属性,像 checkbox, radio, select
等组件,特殊处理。
绑定value和onChange核心代码(有删减)以下:
public bindEvent(value, childList) {
const me = this;
if (!childList || React.Children.count(childList) === 0) {
return;
}
React.Children.forEach(childList, (child) => {
if (!child.props) {
return;
}
const { children, onChange } = child.props;
const bind = child.props['data-name'];
const rules = child.props['data-rules'];
// 分析节点类型,获取对应的属性名是value,仍是checked等
const valuePropName = me.getValuePropName(child);
if (bind) {
child.props[valuePropName] = value[bind];
if (!onChange) {
child.props.onChange = me.onFieldChange.bind(me, bind, valuePropName);
}
}
me.bindEvent(value, children);
});
}
复制代码
onFieldChange的代码:
public onFieldChange(fieldName, valuePropName, e) {
const me = this;
const {
onChange = () => null,
onFieldChange = () => null,
} = me.props;
let value;
if (e.target) {
value = e.target[valuePropName];
} else {
value = e;
}
me.updateValue(fieldName, value, () => {
onFieldChange(e);
const allValues = me.state.formData.value;
onChange(allValues);
})
}
复制代码
上面代码即使实现了咱们的目标,不用手绑 value
和 onChange
了。
演示:
接下来是实现表单验证,表单验证,仍是沿用了 ant-design
的实现,使用async-validator
这个库来作,配置方式和 ant-design
是同样的。为了显示验证的错误信息,加入了 FormItem 容器,使用方式也贴近 ant-design
。
FormItem
的实现使用 React 的 Context API,具体能够查看实现源码,由于不是本文重点,就不说了。
和 ant-design
同样,只要是实现 value
、onChange
接口的组件,均可以在这里使用,不限于原生的 HTML 组件。
经过上面的代码实现咱们想要的目标,不过,仍是有疑问的地方:这个每次渲染都深度遍历子节点,会不会有性能问题?
答案是:影响微乎其微
经过测试,1000
之内的表单控件感觉不到差异。1000
个子组件对 React
来讲,diff算法开销也很大的。
不过,为了提高性能,咱们仍是作了优化,加入了虚拟 DOM 缓存
。
假如咱们在首次渲染后,将建立的虚拟 DOM 缓存下来,第二次渲染就不须要须要从新建立了,也不须要深度遍历节点添加 value
和 onChange
了,可是为了更新 value
,须要获取具备 data-name
节点的引用,将组件以 data-name
值为 key
放到对象里,更新的时候经过 data-name
值获取这个组件,直接更新这个组件的虚拟 DOM
属性就能够了,直接获取 DOM
引用更新 DOM
,这看起来很 JQuery
吧?
经过上面的优化,性能能提高一倍。
不过,若是表单内组件有动态显示、隐藏的话,就不能用虚拟DOM缓存
了,因此,咱们提供了一个属性 enableDomCache
,它能够是布尔值,也能够是一个函数,参数是以前的表单值,由用户对当前值和前值比较,来肯定下次渲染是否使用缓存。不过,只有遇到性能问题的时候能够考虑用它,多数时候没有性能问题,这个 enableDomCache
默认设置为 false
,
示例:
import {Form} form 'form';
export default class FormDemo extends Component<any, any> {
state = {
value: {
name: '',
school: '',
},
}
onFormChange = (value) => {
this.setState({
value,
});
}
onFormSubmit = () => {
// console.log('submit')
}
enableDomCache=(preValue)=>{
const me=this;
const {
value,
} = me.state;
if(preValue.showSchool!==value.showSchool){
return false;
}
return true;
}
render(){
const me=this;
const {
value,
} = me.state;
return (
<Form
value={value}
enableDomCache={me.enableDomCache}
onChange={me.onFormChange}
onSubmit={me.onFormSubmit}
>
<input
data-name={`name`}
data-rules={[ { max: 3, message: '最大长度3', } ]}
type="text"
/>
{
value.showSchool&&(
<input
data-name={`school`}
data-rules={[ { max: 3, message: '最大长度3', } ]}
type="text"
/>
)
}
</Form>
)
}
}
复制代码
若是每次表单的字段修改,都会致使整个表单从新渲染,确实不够完美,因此会有字段分布式管理的想法。
能够考虑给表单加个 redux
的 store
,每一个表单项组件订阅 store
,维护本身的数据状态,表单项之间互不影响,这样表单字段就是分布式的了,store
存储了最新的表单数据。
不过,大多数时候,即便从新渲染,用户也体会不到其中的差异,ant-design
就是从新渲染,这里说的从新渲染,是从新 render
建立虚拟 DOM
,其实 React
进行 diff
后,真是的DOM并未所有渲染。
固然,为了追求完美,避免 React
进行 diff
,那就是最好了,因此对于表单内的重型组件,考虑利用 shouldComponentUpdate
进行更新控制,用过 Redux
同窗都知道,connect
高阶组件内部是作了属性的对比来控制组件是否更新的。
还有一点,受控组件和非受控组件的影响,若是表单自己是受控组件,那么它的属性改变,确定致使自己的从新渲染计算,因此要想更好的性能,最好是使用非受控组件模式,这个仍是要看具体须要,由于目前多数时候,状态都会选择全局状态,非受控组件不会由于外部状态改变而更新,因此可能会有UI状态和全局状态不一致的可能,若是表单数据的修改只有表单自己来控制,那就能够放心使用非受控模式了。
补充,不管是受控和非受控,均可以利用 shouldComponentUpdate
进行组件自己的优化。
在以前的文章讨论中,看到用户对表单嵌套的需求,这个想起来不难,只要表单自己符合 value
onChange
接口,那么表单也能够嵌套表单了,就像下面这样:
import {Form,Input} form 'form';
export default class FormDemo extends Component {
render(){
const me=this;
const {
value,
} = me.state;
return (
<Form value={value} onChange={me.onFormChange} onSubmit={me.onFormSubmit} >
<input data-name="name" type="text" />
<Input data-name="school" type="text" />
<Form name="children1">
<input data-name="name" type="text" />
<Input data-name="school" type="text" />
<Form name="children2">
<input data-name="name" type="text" />
<Input data-name="school" type="text" />
<Form name="children3">
<input data-name="name" type="text" />
<Input data-name="school" type="text" />
</Form>
<Form name="children4">
<input data-name="name" type="text" />
<Input data-name="school" type="text" />
</Form>
</Form>
</Form>
</Form>
)
}
}
复制代码
演示:
虽然实现了表单嵌套,可是这个实现是有问题的,子表单的数据变动,会沿着 onChange
方法逐级向上传递,当数据量大,嵌套层级深的时候,会有性能问题。
嵌套表单数据变动演示:
最好相似于字段的分布式管理同样,每一个表单只负责本身的渲染,不会致使其余表单从新渲染,为了提高性能,咱们进行了优化,提供了 FormGroup
容器,这个容器能够遍历 Form
节点,构建 Form
节点的引用关系,为每一个 Form
生成一个惟一 ID
,将全部 Form
的状态统一由 FormGroup
的 state 管理,至关于进行了扁平化,而不是像原来同样,子级 Form
的 Value
由父级的来管理。
状态偏平化后,每一个表单的变动只会致使自身从新渲染,不影响其余表单。
演示:
可是,上面的优化仅限于非受控状态下,由于受控状态下,仍是要由外部属性传入 value
给 FormGroup
,而内部 value
的和属性传入的 value
结构不一致,一个是扁平的结构,一个树形结构,由树形结构转扁平结构的条件不充分,由于不知道表单的嵌套结构,因此 value
的转换作不到了。
总之,简单的树形结构能够不使用 FormGroup
。复杂的能够考虑使用 FormGroup
,而且设置 defaultValue
而不是 value
,来使用非受控的模式。
本文尝试构建了一个更简洁的表单方案,利用深度遍历子节点
的方法为子组件赋值 value
以及注册 onChange
事件,表单的书写上更加贴近原生,更加简洁,也利用缓存虚拟DOM
的方法对深度遍历子节点这种方式进行了性能优化,尝试实现表单嵌套,而且利用 FormGroup
容器进行数据更新扁平化,不知道你有没有收获。
这看起来很像Vue是吧?,React不像Vue有那么多指令能够辅助,因此表单这块会有那么多的方案来简化,不过想起来,上面的作法和ast的解析执行很相似,虽然不能编译期作,可是运行期作也能够,那么会不会出现一个Template组件,来提供魔法指令?
而后写出下面的代码:
<Template>
<div v-if={true}>
{name}
</div>
<div v-show={true}>
<div/>
</div>
</Template>
复制代码
文章仅供参考,提供解决问题的思路,欢迎你们评论,谢谢!