我们知道 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 的版本