深入理解react-redux
之前在Jarvis OJ的前端开发的时候,使用了react+redux的数据流组织形式,由于redux只是一个通用的Javascript模块,本身和React并没有什么关系,所有就有了一个react-redux的项目,提供了一些api用于react和redux之间更方便的连接和更科学的代码组织方式。记得当时开发的时候,参考了网上许多的教程和例子,但许多文章只是给出了demo或者写法,并未给出为什么要这样写的深层原因,当时开发的时候我也只是依样画葫芦并未深究其所以然,直到最近,我发现突然理解了react+redux为什么要写成这样的深层原因,因此本文本着探究的目的,谈谈我对react+redux项目写法的理解。
在开始之前,先吐槽吐槽,正如一些网友所说的,react的作者信奉unix哲学,希望把react打造成一个小而美的框架,只提供基本的功能,而不像AngularJS那样是一个all-in-one的整体解决方案。也正是因为如此,如果要使用react完成一个交互复杂的庞大单页面应用是远远不够的,还需要借助一些其他的第三方框架,本文所提到的redux就是其中之一。但是经过一番开发体验也会发现,也正如大家所说,在react加上了这些第三方框架之后,整体代码反而变得更加复杂了,比如angularJS只需要100多行就可以完成的一个数据流绑定操作的demo页面,而使用react+redux的方式,一个基本demo就长成下面这样:
这只是一个简单的计数器页面,就有这么多复杂的文件和设定。也就是说,整体看起来变得更加复杂了。这也是许多angular阵营的开发者诟病react的黑点之一。当然,我这里并不想讨论react和angular孰优孰劣的问题,也不想引战,这里仅仅就个人开发体验吐槽吐槽而已。
下面进入正题,首先,对Provider的理解,一个通常的react+redux项目,通常会在app的最外层用Provider再包一层,就像下面这样:
// config app root const history = createHistory() const root = ( <Provider store={store} key="provider"> <Router history={history} routes={routes} /> </Provider> ) // render ReactDOM.render( root, document.getElementById('root') )
而当我们不使用redux的时候,只需要<Route/>那个组件就可以了,这一点如何理解呢,实际上这个并不难理解,开发过react界面就会知道,react的工作原理实际上是父组件将属性,也即property传递给子组件,而最终子组件将自身的property渲染为实际浏览器显示的内容,而组件的state则是与用户操作相关的,当state改变时,整个render()方法都会重新执行,然后将新的dom和当前显示的dom对比,将差异的部分进行更新。所以我们不使用redux的时候,通常会将父组件的state传入子组件,以完成整体页面的动作更新,就像下面这样:
<TextField style={styles.textField} floatingLabelText="E-mail" value={this.state.email} errorText={this.state.emailerr} onChange={this.handleEmailChange} />
TextField就是一个子组件,我们将父组件的回调函数以及state值传入,这样当父组件的state.email改变时,子组件textField的value也会立即改变,改变也会显示在实际的UI界面上。我们知道React中的数据流是单向传递的,只能由父组件传递给子组件,这一点和AngularJS不同,AngularJS的数据流是既可以单向也可以双向绑定的。单向传递这个特性虽然给我们造成了一些不便,但却可以使代码更易读更易维护,数据流以及state的变化更易于理解。所以通常来说,我们在一个应用里,会碰到的所有情况可以归结为以下三种:
1.父组件向子组件传递数据
2.子组件向父组件传递数据
3.子组件和子组件之间传递数据
我们知道1是最自然的情况,react原生就是这样的数据传递模式,而2的话,可以使用回调函数来解决。而3是最麻烦的情况,我们要解决3,通常会选择用父组件再将这2个子组件包一下,然后子组件分别向父组件的回调函数传递数据,然后再通过父组件根据相应的回调函数操作改变自身的state,传递给相应的子组件,这样子组件之间就可以交互数据了。虽然这样也能解决问题,但是不同人写的代码参差不齐,而且花样百出,可读性会很差,难以维护,在这时候,就要请出我们的redux了,其实也不止是redux,类似的数据流框架还有flux和reflux也能解决问题,框架的目的其实是为了更好的去组织代码,易于理解和方便维护,仅此而已。如果愿意自己造轮子解决,就像我上面说的,用父组件去包裹,不用redux也是完全可以的。redux只是提供了一个优雅的解决方案和设计思路而已,其被设计的目的也主要是为了解决上面的第3种情况的。
说到这里,Provider的作用就显而易见了,由于redux全局只有1个store,而在react中,数据流只能由父组件传递给子组件,因此,Provider就是这样一个“超级父组件”,它是整个根节点的父组件,store就由这个“超级父组件”向所有子组件传递。所以你会发现,使用了redux之后,所有组件都只需要设定propeties而不再需要state了。
下面就是一个典型的使用了redux之后子组件的写法:
import React,{Component,PropTypes} from 'react'; class Counter extends Component { static propTypes = { //increment必须为fucntion,且必须存在 increment: PropTypes.func.isRequired, incrementIfOdd: PropTypes.func.isRequired, incrementAsync: PropTypes.func.isRequired, decrement: PropTypes.func.isRequired, //counter必须为数字,且必须存在 counter: PropTypes.number.isRequired }; render() { //从组件的props属性中导入四个方法和一个变量 const { increment, incrementIfOdd, incrementAsync, decrement, counter } = this.props; //渲染组件,包括一个数字,四个按钮 return ( <div> Clicked: {counter} times {' '} <button onClick={increment}>+</button> {' '} <button onClick={decrement}>-</button> {' '} <button onClick={incrementIfOdd}>Increment if odd</button> {' '} <button onClick={() => incrementAsync()}>Increment async</button> </div> ); } } export default Counter;
其中,渲染组件的状态使用的是props去渲染的,因为这时候所有子组件的properties,都被Provider所传递的store中的state所设定了,所以只需要properties就可以了,而两个类型是func的属性,其实就是回调函数。然而,正是因为单向数据流的关系,当子组件层数非常深的时候,此时在每个组件里都加入一个用来传递store的属性,难免这种写法有点太冗余了。所以,这时候就要请出我们的connect方法了。connect方法的目的就是将子组件和store直接connect在一起,而不用每一层都去传递这个store了,实际也相当于一个语法糖的作用。
于是,使用了redux之后,子组件通常会用下面这样的一个Container包起来:
import {bindActionCreators} from 'redux'; import {connect} from 'react-redux'; import Counter from '../components/Counter'; import * as CounterActions from '../actions/counter'; function mapStateToProps(state) { return { counter: state.counter, } } function mapDispatchToProps(dispatch) { return bindActionCreators(CounterActions,dispatch); } export default connect(mapStateToProps,mapDispatchToProps)(Counter);
其中,connect方法的2个参数分别是mapStateToProps和mapDispatchToProps,然后最后传入我们的Counter组件。这时候,就很容易理解了,mapStateToProps的作用就在于从store中取出渲染这个组件所需要的state,将其注入该子组件的Props,而redux中的Dispatch作用就是发起一个action让reducer处理,就相当于前面提到的,父组件的回调函数的作用,也就是说,mapDispatchToProps的作用就在于将父组件的回调函数也作为Props传入子组件。这就是我前面所述的“子组件通过父组件回调函数改变父组件状态,进而改变自身或者其他子组件Props来实现子组件间的数据传递”这个实现思路的一种实现而已,到现在,应该对于react-redux每个操作的意义就很容易理解了。
至于action和reducer,实际上是一种回调函数的组织形式而已,每个action发出的dispatch用于区分这个操作要完成什么,而reducer根据具体要完成的工作来改变store里的内容,这样的话,就可以更清晰的去组织回调函数(在这里叫reducer),而不必对于每个功能,都自己在父组件中自己命名一个过几天就忘了要干啥的回调函数,相比之下,用action+dispatch+reducer的模式去组织代码显然比自己随便命名回调函数要有章法的多。
至于其他middleware的功能,主要是为了解决一些细节方面的处理,比如redux-thunk用于解决异步action的问题,还有一些将reducer组合在一起的功能等等,这些都属于细节方面的实现,其实思路还是非常简单明了的。
最后说说结论吧,说白了react-redux就是实现了将所有子组件用超级父组件包裹,然后通过回调函数解决单向数据流中子组件之间的耦合问题,并用更优雅易于理解和维护的方式将代码组织起来,这就是react-redux的精髓所在。
当然,笔者水平有限,本文也仅仅处于笔者个人开发体会和观点,有任何其他观点或者本文中有任何疏漏或错误之处,也欢迎一起探讨。
关于文中示例项目的完整代码,大家可以在这里下载。