我们知道 React 的标准模式是单向数据流,而其表单项通常需要监听 onChange 事件,然后通过改变外部的 value 来回写表单项的 value,譬如如下 input
class App extends React.Component { constructor( props ) { super( props ); this.state = { inputValue: 'default' } this.inputChangeHandler = ( e )=>{ this.setState( { inputValue: e.target.value } ); } } render() { return ( <div> <form> <input value={ this.state.inputValue } onChange={ this.inputChangeHandler } /> </form> </div> ) } }
如果表单有很多表单项,那么这种标准的做法需要你写很多个 state 的属性和很多个 onChange 监听函数,这是一个体力活儿。但是一般的表单应用其实不需要实时监控表单项的用户输入,用 defaultValue 足以,在表单项目 onBlur 或者最后提交的时候一次验证获取用户输入即可,譬如:
class App extends React.Component { constructor( props ) { super( props ); this.submit = ( e )=>{ let userInputValue = this.refs.userInput.value; // 1. 验证 userInputValue // 2. 提交表单 } } render() { return ( <div> <form> <input ref="userInput" defaultValue="default" /> <button onClick={ this.submit }>提交</button> </form> </div> ) } }
这样就可以少写不少代码,当然你可以写一些工具去批量添加所有的 onChange 事件监听函数和对应的 state 的属性,譬如 redux-form。(回头一想,这种写法在提交时候也需要写很多获取用户输入的代码,如果使用第一种正模式,那么提交时候只需要获取 state 就可以了,不过这里先不讨论这些)<br/>
对于一个表单而言,通常还需要重置功能(reset),如果是第一种正模式的写法,我们只要保存一份初始化的默认值,在用户点击到了重置后,通过 setState 设回去就行了。但是如果使用第二种 defaultValue 的写法,那么就没有办法了,因为 defaultValue 只在第一次创建虚拟 dom 的时候有作用,如果 dom 不改变你改变 defaultValue 是没有用的。这个时候该怎么办呢?<br/>
嘿嘿!这个时候我们就可以用到这个奇技淫巧了。既然 defaultValue 是在创建虚拟 dom 的时候有用,那么我们在用户点击重置的时候让 React 重新创建这些表单项的虚拟 dom 不就好了么。根据 React 虚拟 dom diff 的算法,只要改变 dom 节点的类型就能促使在 diff 的时候重新创建虚拟 dom。具体的写法我们就用代码来演示下:
class App extends React.Component { constructor( props ) { super( props ); // fieldSetWrapperType 是一个标志位属性,render 中会根据这个变量的值的不同,渲染不同的元素 this.fieldSetWrapperType = 'div'; this.submit = ( e )=>{ let userInputValue = this.refs.userInput.value; // 1. 验证 userInputValue // 2. 提交表单 } this.reset = ()=>{ // 点击重置,改变标志位 this.fieldSetWrapperType = this.fieldSetWrapperType === 'div' ? 'section' : 'div'; // 强制刷新这个组件 this.forceUpdate(); } } // 把表单项的渲染抽象到一个方法中,避免重复编码 renderFieldSet() { return ( <input ref="userInput" defaultValue="default" /> ); } render() { return ( <div> <form> { /* 根据 fieldSetWrapperType 值的不同,渲染不同的元素(表单项的 wrapper 元素) */ this.fieldSetWrapperType === 'div' ? <div className="wrapper">{ this.renderFieldSet() }</div> : <section className="wrapper">{ this.renderFieldSet() }</section> } <button onClick={ this.submit }>提交</button> <button onClick={ this.reset }>重置</button> </form> </div> ) } }
思路就是在表单项外面包一层元素,每次点击重置后改变一个变量,再强制刷新这个组件,组件根据这个变量不同的值把这个包装元素的 type 改变,那么它下面的所有表单项的虚拟 dom 都会被重新创建,达到了重置的目的。不过这个效果依赖于 React 虚拟dom diff 算法。如果以后算法改变了,那么可能就失效了,而且这个写法是反模式的,我初衷是想在处理巨型表单时候少写点代码偷懒用。如果使用时候出现什么副作用,鄙人概不负责。
此技巧在写文章时 React 正处于 15.4.x 的版本