在react
中, 如果不使用双向绑定, 那么获取一个受控制的input
的数据需要写一个onChange
回调函数来获取输入值, 再通过setState
来将value
传入input
做展示. 少量的输入框的情况下, 还可以使用这种方法, 但是如果在比较大和复杂的表单中, 这种写法就会觉得太麻烦了.
旧版本的valueLink
在以前的版本中, 可以通过mixin:[LinkedStateMixin]
这种方法来实现简易的双向绑定, 它的底层方法是react
为input
提供的valueLink
属性, 可以通过传递一个含有value
和requestChange
属性的对象, 来实现value
和onChange
的同时绑定. 但是在react 15
中, valueLink
已经被标记为deprecated
, 如果你继续使用, 会出现提示:
Note: LinkedStateMixin is deprecated as of React v15. The recommendation is to explicitly set the value and change handler, instead of using LinkedStateMixin.
而在react 16
这个版本中, valueLink
已经正式消失了, 对此, react
官方的说法是, No easy/messy 2-way data binding with modern React!
, 不过这只是说说而已, 来看看新版本的双向绑定怎么实现比较好.
新版本使用 当不使用双向绑定的时候, react
的做法是为input
提供value
和onChange
属性, 那么如果想简写时, ES6
的扩展运算符...
则提供了很大的方便性, 我们只需要返回一个含有value
和onChange
的对象即可.
1 2 3 4 5 6 7 function twoway ( ) { return { value : 'value' , onChange : function , } } <input {...twoway()}/>
这便是最基础的用法了, 那么基于此来扩展一下. 首先, value
所在key
肯定是需要作为参数传入的, 而读取和设置state
的方法, 也是组件特有的. 那么我们需要直接把组件实例传入即可.
1 2 3 4 5 6 7 8 9 10 11 function twoway (component, key ) { return { value : component.state[key], onChange : (e ) => { component.setState({ [key]: event.target.value, }); }, }; } <input {...twoway(this , 'testValue' )} />;
加入path
好了, 初步的双向绑定方法就完成了, 下一步来优化它. 首先, 不应该所有的value
都是直接保存在state
上, 可能是保存在state
中某个对象上, 那么所在对象的深层path
也应该作为参数传入, 而key
是作为state
的key
传入.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 import _ from 'lodash' ;function twoway (component, key, path ) { let hasPath = _.isArray(path) && path.length > 0 ; if (!hasPath) { return { value : component.state[key], onChange : (event ) => { component.setState({ [key]: event.target.value, }); }, }; } else { return { value : _.get(component.state[key], path), onChange : (event ) => { component.setState({ [key]: { ..._.set(component.state[key] || {}, path, event.target.value) }, }); }, }; } } class Test extends React .Component { constructor (props ) { super (props); this .state = {}; } render ( ) { return ( <div > <input {...twoway (this , 'testValue ')} /> <input {...twoway (this , 'testValueObject ', ['Lv1 ', 'Lv2 '])} /> <div > testValue : {this.state.testValue}</div > <div > testValueObject : {JSON.stringify(this.state.testValueObject)}</div > </div > ); } }
加入immutable
支持 而我们推荐整个项目里面的对象尽量使用immutable
结构, 达到优化性能和避免state
对象深度拷贝的目的, 那么twoway
方法也需要加入对immutable
结构的支持. 顺便也加入对checkbox
的支持.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 import _ from 'lodash' ;import Immutable from 'immutable' ;function twoway (component, key, path ) { if (!key) { throw 'dataValueBindHandler to bind state need one key' ; } const bindFun = (event ) => { const ele = event.target; const value = ele.type === 'checkbox' ? ele.checked : ele.value; const stateValue = component.state[key] || {}; if (Immutable.isCollection(stateValue)) { const setFuncName = _.isArray(path) ? 'setIn' : 'set' ; component.setState({ [key]: path ? component.state[key][setFuncName](path, value) : value, }); } else { component.setState({ [key]: path ? { ..._.set(stateValue, path, value) } : value, }); } }; let value = '' ; if (Immutable.isCollection(component.state[key])) { value = path ? component.state[key][_.isArray(path) ? 'getIn' : 'get' ](path) : component.state[key]; } else { value = path ? _.get(component.state[key], path) : component.state[key]; } return { value : value, onChange : bindFun, }; } class Test extends React .Component { constructor (props ) { super (props); this .state = { testValue : '' , testValueObject : { Lv1 : { Lv2 : 'val' , }, }, testValueImmutableObject : Immutable.Map().setIn(['Lv1' , 'Lv2' ], 'valImmutable' ), }; } render ( ) { return ( <div > <input {...twoway (this , 'testValue ')} /> <input {...twoway (this , 'testValueObject ', ['Lv1 ', 'Lv2 '])} /> <input {...twoway (this , 'testValueImmutableObject ', ['Lv1 ', 'Lv2 '])} /> <div > testValue : {this.state.testValue}</div > <div > testValueObject : {JSON.stringify(this.state.testValueObject)}</div > <div > testValueImmutableObject : {JSON.stringify(this.state.testValueImmutableObject)}</div > </div > ); } }
加入onChange
方法缓存 对于这个函数, 还可以再优化一下, 因为每次执行twoway
方法, 都会生成一个新的bindFun
函数, 如果你的input
使用的是自定义组件, 则会因为改变props
中onChange
变化而不是value
变化而执行render
函数, 那么需要将onChange
变成一个固定的函数, 不需要每次重新生成, 则不会因为变化onChange
而不是value
时触发子组件的render
函数了.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 import React from 'react' ;import _ from 'lodash' ;import Immutable from 'immutable' ;const dataValueBindHandler = (component, key, path ) => { if (!key) { throw 'dataValueBindHandler to bind state need one key' ; } let dataHandleBindMap = component.__dataHandleBindMap || Immutable.Map(); let mapKey = `key:${key} ` ; if (path) { mapKey += `::path:${_.isArray(path) ? path.join('__' ) : path} ` ; } let onChange = null ; if (dataHandleBindMap.has(mapKey)) { onChange = dataHandleBindMap.get(mapKey); } else { const bindFun = (event ) => { const ele = event.target; const value = ele.type === 'checkbox' ? ele.checked : ele.value; const stateValue = component.state[key] || {}; if (Immutable.isCollection(stateValue)) { const setFuncName = _.isArray(path) ? 'setIn' : 'set' ; component.setState({ [key]: path ? component.state[key][setFuncName](path, value) : value, }); } else { component.setState({ [key]: path ? { ..._.set(stateValue, path, value) } : value, }); } }; component.__dataHandleBindMap = dataHandleBindMap.set(mapKey, bindFun); onChange = bindFun; } let value = '' ; if (Immutable.isCollection(component.state[key])) { value = path ? component.state[key][_.isArray(path) ? 'getIn' : 'get' ](path) : component.state[key]; } else { value = path ? _.get(component.state[key], path) : component.state[key]; } return { value : value, onChange : onChange, }; }; export default dataValueBindHandler;class Test extends React .Component { constructor (props ) { super (props); this .state = { testValue : '' , testValueObject : { Lv1 : { Lv2 : 'val' , }, }, testValueImmutableObject : Immutable.Map().setIn(['Lv1' , 'Lv2' ], 'valImmutable' ), }; } render ( ) { return ( <div > <input {...dataBindHandler (this , 'testValue ')} /> <input {...dataBindHandler (this , 'testValueObject ', ['Lv1 ', 'Lv2 '])} /> <input {...dataBindHandler (this , 'testValueImmutableObject ', ['Lv1 ', 'Lv2 '])} /> <div > testValue : {this.state.testValue}</div > <div > testValueObject : {JSON.stringify(this.state.testValueObject)}</div > <div > testValueImmutableObject : {JSON.stringify(this.state.testValueImmutableObject)}</div > </div > ); } }