最近項目裏在作咱們本身的組件庫,關於表單這塊,如何實現一個更簡單的表單方案,是咱們一直在討論的問題,以前項目裏習慣用 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>
複製代碼
文章僅供參考,提供解決問題的思路,歡迎你們評論,謝謝!