react在使用的过程当中,主要是利用组件内的state来存储状态,在多个组件共享数据的时候,每一个组件内都得保存相同一份数据,会形成数据重复冗余;而利用props进行组件间的通信,在组件比较简单的时候,该方法倒没什么不妥,但随着咱们的应用愈来愈大,愈来愈复杂,单纯的靠props进行组件间的通信,会增长代码的复杂度和可读性,但查询数据源bug的时候,会变得极其复杂;更严格的数据流控制,是解决这个问题的所在。css
让咱们逐一解释一下三个原则。node
惟一数据源是指应用的状态数据只存在惟一的Store上,若是数据存在多个store上,容易形成数据冗余,并且也容易致使数据一致性方面出现问题,并且,多个Store上面的数据若是存在依赖性,会增长应用的复杂程度,容易带来新的问题;固然,Redux并无阻止一个应用拥有多个store,但这样不只没有任何好处,甚至还不如一个store更容易组织代码。
这个惟一Store上的状态就是一个树形的形象,每一个组件上每每只是树形对象上的一部分,而如何设计Store上的树形对象,就是Redux中的核心问题。react
要驱动用户界面渲染,就要改变应用的状态,保持状态只读,就是说不能直接去修改状态,要修改Store上的状态,须要经过派发一个action去处理;action会建立一个新的状态对象返回给Redux,有Redux完成新的状态的组装。npm
所谓纯函数,就是不对外界产生任何反作用的函数;这里所说的纯函数,就是Reducer;Reducer并非Redux的特有术语,是计算机的一个通用概念,就比如是JavaScript中的reduce(fn,init)函数,里面接收的回调函数fn就是一个Reducer;
在Redux中,每一个reducer的函数签名以下所示:redux
reducer(state, action)
第一个参数state是当前的状态,第二个参数action是收到的action对象,而reducer函数所要作的事情,就是根据state和action的值产生一个新的对象返回,注意reducer必须是一个纯函数,也就是说,函数的返回结果只能由state和action决定,并且不产生任何反作用,也不能修改参数state和action对象。
例如:浏览器
function reducer(state, action) => { const {targetKey} = action; //获取目标key的值 switch (action.type) { case ActionTypes.typeOne: return {...state, [targetKey]: state[targetKey] + 1}; case ActionTypes.typeTwo: return {...state, [targetKey]: state[targetKey] - 1}; default: return state } }
从上面的例子,能够看出,reducer函数不只接收action,还接收state为参数,这就是说,Redux只负责计算状态,不负责保存状态。服务器
为了方便,直接用create-react-app工具来初始化项目,执行下面指令前,必须保证咱们的电脑已经安装了node.js;数据结构
npm install --global create-react-app
在命令行窗口中执行以上语句,安装create-react-app工具,安装成功后,能够获得如截图的内容;
接下来咱们在命令行执行下面的指令,建立测试使用的应用;架构
create-react-app react-redux-app
建立成功后,进入项目目录,启动应用;app
cd react-redux-app npm start
这个命令启动一个开发者模式的服务器,同时也会让你的浏览器自动打开一个网页,指向本机http://localhost:3000/
create-react-app指令安装成功截图:
启动应用成功截图:
应用启动后的初始界面:
由于我的习惯,通常我都会执行 npm run eject,该指令的做用是,就是把潜藏在react-scripts 中的一系列技术找配置都“弹射”到应用的顶层,而后就能够研究这些配置细节了,并且能够更灵活地定制应用的配置。在react和redux结合使用的时候,没有理由不选择使用react-redux库,这样能大大节省代码的书写,不过从一开始咱们不直接使用它,否则会对其内部设计一头雾水,因此先从最简单的redux开始使用,一步步改进,循循渐进地过分到react-redux。下面是项目目录结构:
其中,Store.js至关于MVC架构里面的M,views文件夹至关于V,Reducer.js至关于C,至于Actions和ActionTypes,能够理解用用户的行为;接下来须要执行npm install redux安装redux,咱们从入口文件讲解实例的内容;
首先是index.js文件,文件先引入react和react-dom,再将ControlPanel组件挂载渲染到目标div;
import React from 'react'; import ReactDOM from 'react-dom'; import ControlPanel from './views/ControlPanel' import './index.css'; ReactDOM.render(<ControlPanel />, document.getElementById('root'));
在Store.js文件中,经过引入redux的createStore函数,以及Reducer处理函数,建立并返回一个store,createStore(reducer, initValues)中的reducer是处理派发出来的action函数,initialValues为初始值,也就是组件所共享的数据结构;
import {createStore} from 'redux' import reducer from './Reducer' const initValues = { 'First': 0, 'Second': 10, 'Third': 20} const store = createStore(reducer, initValues) export default store
在Reducer.js中,经过处理派发出来的action,动态的修改目标数据,并返回一个新的对象,须要注意的是,Redux 中把存储state 的工做抽取出来交给Redux 框架自己, 让reducer 只用关心如何更新state , 而不要管state 怎么存,因此每次修改后都须要合并以前的state后返回,保证数据的一致性。redeucer函数接收两个参数,第一个为state,即store中的旧的状态,第二个参数action是派出出来的对象,上面会携带想作的操做类型和所携带的数据;
import * as ActionTypes from './ActionsTypes' export default (state, action) => { const {counterCaption} = action switch (action.type) { case ActionTypes.INCREMENT: // 利用...展开运算符,合并生成新的状态对象返回 return {...state, [counterCaption]: state[counterCaption] + 1} case ActionTypes.DECREMENT: return {...state, [counterCaption]: state[counterCaption] - 1} default: //默认返回当前的状态,不作修改 return state } }
最后是Actions和ActionTypes,咱们把ActionTypes抽出来单独写,能够更好的复用代码以及增长代码的可读性,每一个action都返回一个对象,对象有个名为type的参数,存放action类型,其余字段为须要修改的参数;
ActionTypes.js
export const INCREMENT = 'increment' export const DECREMENT = 'decrement'
Actions.js
import * as ActionTypes from './ActionsTypes' export const increment = (counterCaption) => { return { type: ActionTypes.INCREMENT, counterCaption: counterCaption } } export const decrement = (counterCaption) => { return { type: ActionTypes.DECREMENT, counterCaption: counterCaption } }
最后来实现一下在组件中怎么去引用咱们所建立的store,先上代码:
ControlPanel.js
import React, {Component} from 'react' import Counter from './Counter' import Summary from './Summary' const style = { margin: '20px'}; class ControlPanel extends Component { render() { return ( <div style={style}> <Counter caption='First' /> <Counter caption='Second' /> <Counter caption='Third' /> <hr/> <Summary /> </div> ) } } export default ControlPanel
Summary.js
import React, {Component} from 'react' import store from '../Store' class Summary extends Component { constructor(props) { super(props); this.onChange = this.onChange.bind(this) this.state = this.getOwnState() } onChange() { this.setState(this.getOwnState()) } getOwnState() { const state = store.getState() let sum = 0 for (const key in state) { if (state.hasOwnProperty(key)) { sum += state[key] } } return {sum: sum} } shouldComponentUpdate(nextProps, nextState, nextContext) { return nextState.sum !== this.state.sum } componentDidMount() { store.subscribe(this.onChange) } componentWillUnmount() { store.unsubscribe(this.onChange) } render() { const sum = this.state.sum return ( <div>Total: {sum}</div> ) } } export default Summary;
Counter.js
import React, {Component} from 'react' import PropTypes from 'prop-types' import store from '../Store' import * as Actions from '../Actions' const buttonStyle = { margin: '10px'}; class Counter extends Component { constructor(props) { super(props) this.onIncrement = this.onIncrement.bind(this) this.onDecrement = this.onDecrement.bind(this) this.onChange = this.onChange.bind(this) this.getOwnState = this.getOwnState.bind(this) this.state = this.getOwnState() } getOwnState() { return { value: store.getState()[this.props.caption] } } onIncrement() { store.dispatch(Actions.increment(this.props.caption)) } onDecrement() { store.dispatch(Actions.decrement(this.props.caption)) } onChange() { this.setState(this.getOwnState()) } shouldComponentUpdate(nextProps, nextState, nextContext) { return (nextProps.caption !== this.props.caption) || (nextState.value !== this.state.value) } componentDidMount() { store.subscribe(this.onChange) } componentWillUnmount() { store.unsubscribe(this.onChange) } render() { const value = this.state.value; const {caption} = this.props; return ( <div> <button style={buttonStyle} onClick={this.onIncrement}>+</button> <button style={buttonStyle} onClick={this.onDecrement}>-</button> <span>{caption} count: {value}</span> </div> ) } } Counter.propTypes = { caption: PropTypes.string.isRequired } export default Counter
从代码中能够看出,当须要获取store中的状态的时候,能够经过store.getState()来获取store中state状态值,并在constructor中对this.state作初始化赋值,这样组件就能获取到初始数据;当须要派发一个action的时候,能够调用store.dispatch()来派发一个action,参数为导入的Actions.js文件中export的对象;咱们还须要保持store和this.state的同步,在componentDidMount函数中,经过Store的subscribe监听其变化,只要Store状态发生变化,就会调用这个onChange方法;在componentWillUnmount函数中,须要把这个监听注销掉,防止内存泄漏;到这里,就简单实现了经过redux来共享数据,操做后效果以下:
经过上面的例子,咱们能够发现一个规律,在Redux框架下,一个React组件基本上是完成如下两个功能:
根据组件拆分的原则,一个组件只负责一件事情,因此能够考虑,把例子上的组件再拆分红两个组件,分别承担一个任务,而后把两个组件嵌套起来,完成本来一个组件完成的全部任务;在这样的关系下,两个组件是父子组件的关系。在业界中,承担第一个任务的组件,也是负责和Redux Store打交道的组件,处于外层,因此被叫作容器组件(聪明组件),对于承担第二个任务的组件,也是只负责渲染界面的组件,处于内层,叫作展现组件(傻瓜组件),它是一个纯函数。关系图以下,容器组件负责和Store打交道,获取数据后,经过props传给展现组件,展现组件再渲染出对应的界面;
咱们能够对上面的例子中的Counter组件进行拆解分析,把原有的Counter拆分为两个组件,分别为展现组件Counter和容器组件CounterContainer;展现组件Counter就会变得很简单了,只须要接收props并将之渲染出来便可;
calss Counter extends Component { constructor(props) { super(props) } render( const {caption, onlncrement , onDecrement , value) = this.props; return ( <div> <button style=(buttonStyle) onClick={onincrement)>+</button> <button style={buttonStyle) onClick={onDecrement)>-</button> <spa n>{caption} count : (value}</span> </div> ) ) }
对于无状态组件,能够进一步缩减代码,React支持只用一个函数表示的无状态组件,因此能够进行进一步缩减;
function Counter (props) { const {caption,onincrement, onDecrement, value} = props; return ( <div> <button style=(buttonStyle) onClick={onincrement)>+</button> <button style={buttonStyle) onClick={onDecrement)>-</button> <spa n>{caption} count : (value}</span> </div> ) }
对于这种写法,获取props的值的方式再也不是经过this.props来获取了,而是经过参数props获取,还有一种经常使用写法,就是把props的结构赋值直接放在参数中,能够再节省一行的代码量;
function Counter ({caption, onincrement, onDecrement , value} { ... }
而对于容器组件CounterContainer,前面部分基本保留原有的Counter的方法声明和生命周期的声明,主要修改的是render函数返回的渲染内容;
class CounterContainer extends Component { ...... render( return <Counter caption={this.props.caption} onincrement={this.onincrement} onDecrement={this.onDecrement} value={this . state .value} /> ) }
接下来,咱们须要再研究另一个问题,就是如今都是哪里使用到Redux Store就直接导入Store,这样直接导入早晚会有问题;像在实际开发中,可能会经过npm引入第三方组件库,当开发一个独立的系统的时候,咱们都不知道这个组件会在哪一个位置,固然不可能知道预先定义惟一的Redux Store的文件位置了,因此直接导入Store是很是不利于组件的复用的;React提供了一个叫作Context的功能,能完美解决这个问题。
所谓Context,就是上下文环境,让一个树状组件上有一个全部组件都可以访问的对象,为了完成这个任务,须要上下级组件的配合。这个上级组件之下的全部子组件,只要宣称本身须要这个context,就能够经过this.context访问到这个共同的环境对象;因此须要建立一个拥有store的顶层组件,他是一个通用的context提供者,能够在其下的全部子组件中访问到context;咱们暂时把这个组件称为Provider;
class Provider extends Component { getChildContext () { return { store: this.props.store } } render () { return this.props.children } }
Provider的做用就是把子组件给渲染出来,在渲染中,Porvider 不作任何的处理;this.props.children是指两个标签以前的子组件,好比<Provider><ControlPanel /></Provider>,this.props.children指的就是<ConrolPanel />;除了把渲染工做交给子组件,Provider还提供了一个函数getChildContext,这个函数返回的就是表明Context的对象。为了让React承认Provider为一个Context的提供者,还须要指定Provider的childContextTypes属性,代码以下:
Provider.childContextTypes = { store: PropTypes.object }
Provider 还须要定义类的childContextTypes ,必须和getChildContext 对应,只有这二者都齐备, Provider 的子组件才有可能访问到context 。
import store from ’ ./Store .js ’ import Provider from ’. /Provider . js’ ReactDOM . render( <Provider store={store }> <ControlPanel /> </Provider> , document . getElementByid ( ’ root ’) )
为了让CounterContainer可以访问到context,必须给CounterContainer类的ContextTypes赋值和Provider.childContextTypes同样的值二者必须一致,否则访问不到context,代码以下:
CounterContainer.contextTypes ={ store: PropTypes.object }
在CounterContainer 中,全部对store的访问,都是经过this.context.store完成的,由于this.context就是Provider提供的context对象,因此getOwnState函数代码以下:
getOwnState () { return { value: this.context.store.getState()[this.props.caption] } }
最后,由于咱们是本身定义构造函数的,经过this.context访问上下文,因此constructor中须要多接收一个参数
constructor (props, context) { super(props, context) }
这里有个小技巧,能够一劳永逸的解决参数个数问题,不须要由于每次参数个数不一样而屡次修改代码,就是利用arguments和...展开运算符,以下:
constructor () { super(...arguments) }
至此,上面已经讲解了两个能够改进React 一次来适应Redux 的方法,第一是把一个组件拆分为容器组件和傻瓜组件,第二是使用React 的Context 来提供一个全部组件均可以直接访问的Context ,也不难发现,这两种方法都有套路,彻底能够把套路部分抽取出来复用,这样每一个组件的开发只须要关注于不一样的部分就能够了。
实际上,已经有这样的一个库来完成这些工做了,这个库就是react-redux。
须要使用npm install react-redux --save
安装react-redux库,安装完成后,须要作对一下三个文件作修改,首先是index.js文件,代码以下
import React from 'react'; import ReactDOM from 'react-dom'; import {Provider} from 'react-redux' import ControlPanel from './views/ControlPanel' import store from './Store'import './index.css'; ReactDOM.render( <Provider store={store}> <ControlPanel/> </Provider>, document.getElementById('root') );
咱们须要在index中引入Provider,做为context的提供者,并将store做为props传进去,这里的思路就跟改进react的第二种方法同样;
在具体的组件中,须要怎么样去获取到store中的数据,下面以counter为例讲解一下;
import React from 'react' import PropTypes from 'prop-types' import * as Actions from '../Actions' import {connect} from 'react-redux' const buttonStyle = { margin: '10px'}; function Counter({caption, onIncrement, onDecrement, value}) { return ( <div> <button style={buttonStyle} onClick={onIncrement}>+</button> <button style={buttonStyle} onClick={onDecrement}>-</button> <span>{caption} count: {value}</span> </div> ) } Counter.propTypes = { caption: PropTypes.string.isRequired, onIncrement: PropTypes.func.isRequired, onDecrement: PropTypes.func.isRequired, value: PropTypes.number.isRequired } function mapStateToProps(state, ownProps) { return { value: state[ownProps.caption] } } function mapDispatchToProps(dispatch, ownProps) { return { onIncrement: () => { dispatch(Actions.increment(ownProps.caption)) }, onDecrement: () => { dispatch(Actions.decrement(ownProps.caption)) } } } export default connect(mapStateToProps, mapDispatchToProps)(Counter);
这里主要的修改是引入和使用了connect组件,connect是react-redux提供的一个函数,这个方法接收两个参数,mapStateToProps和mapDispatchToProps,执行结果依旧是一个函数,因此后面才继续跟着一个括号和参数,实际上这里就是后面会学习到的高阶组件;这里两次函数执行,第一次是connect函数的执行,第二次是把connect函数返回的函数再次执行,最后产生的就是容器组件,至关于前面所讲的CounterContainer;connect的具体工做就是把Store上的状态转化为内层傻瓜组件的prop,把内层傻瓜组件中用户动做转化为派送给Store的动做;对于例子中的mapStateToProps和mapDispatchToProps函数,名称是能够随便起的,只不过此处是用了业界习惯用法,这两个函数均可以包含第二个参数,表明的是ownProps,也就是直接传递给外层容器组件的props;
Redux 是F lux 框架的一个巨大改进,Redux强调单一的数据源,保持状态只读和数据改变只能经过纯函数完成的原则,和React的UI=render(state)的思想完美契合。在这一块学习中,利用Counter循循渐进,为了就是更清晰的理解每一个改动背后的动因,最后,咱们终于通react-redux 完成了React 和Redux 的融合。