本文的目的很简单,介绍Redux相关概念用法 及其在React项目中的基本使用javascript
假设你会一些ES6、会一些React、有看过Redux相关的文章,这篇入门小文应该能帮助你理一下相关的知识php
通常来讲,推荐使用 ES6+React+Webpack 的开发模式,但Webpack须要配置一些东西,你能够先略过,本文不须要Webpack基础html
入门,只是一些基础概念和用法的整理,更完整的内容推荐去看看文档,英文,中文java
(不过我我的认为,官方文档的例子相对来讲太复杂了,很难让新手立刻抓住重点)react
(官方的例子正统且联系业务,不一样类型的操做或数据放在不一样文件中,很规范,但也很绕,因此本文使用的例子很是简单,且直接放在一个文件中 以便于理解)git
搭飞机前往:github
Flux思想、Redux基本概念、Redux的使用、Redux在React中的使用(同步)、Redux在React中的使用(异步,使用中间件)ajax
Flux是一种概念思想,或者说是一种应用架构npm
根据它的概念,一个应用中的数据流动应是单向的,且应用中的全部数据保存在一个位置,数据变化时保证视图也同步变化,保证了数据和视图的状态是一一对应起来的json
此应用应该分为四层:
它的概念思想可能一时半会理解不了,不要紧,过段时间就行了
上面说到,Flux只是一个思想,咱们能够根据这个思想来本身实现出一个技术方案,来解决问题
是要解决什么问题呢?
在使用React的过程当中,在组件间通讯的处理上咱们用了回调的方式,若是组件层级很深,不一样组件间的数据交流就会致使回调及其触发的函数很是多,代码冗杂
须要一个状态管理方案,方便管理不一样组件间的数据,及时地更新数据
而Flux思想中的Store层,切合了这个问题
Redux是受Flux启发实现的一个技术方案,能够认为它是Flux的产物,但它并无沿用Flux全部的思想
主要区别是Flux的派发器dispatcher,Redux认为使用派发器就得增长事件订阅/发布的规则,倒不如直接用函数调用的方式来得实在,简单而统一,因此就将处理action的任务交给了store层(直接调用这个对象的dispatch方法)
Redux说简单简单,由于也就几个API,理解好概念就好用了;说复杂也复杂,由于它将一个应用分红了不一样部分(action、处理action、store数据等),在正规的项目中是推荐将各部分区分到不一样文件中的(如官方的例子),文件数量不少可能会比较难管理,固然,细粒化了也就减小了耦合度。最后还要加个操做把Redux的数据更新给React组件(若是用了React)
在大多数状况下,Redux是不须要用的,如UI层很是简单,没有太多互动的
而在多交互,多数据源的时候能够考虑使用
在须要管理复杂组件状态的时候,能够考虑使用
上面讲了那么多字,仍是看代码来得实在
这里先纯粹讲Redux,毕竟它和React是没啥关系的
首先是环境配置,基本上都会使用ES6,因此Babel的支持是必须的
而后是Redux的支持,若是使用Webpack打包编译,就用npm安装个redux包
这里采用直接在浏览器引入的方式,使用 这个库
<body> <div id="box"></div> <script type="text/javascript" src="../lib/react.js"></script> <script type="text/javascript" src="../lib/react-dom.js"></script> <script type="text/javascript" src="../lib/redux.min.js"></script> <script type="text/javascript" src="../build/reduxStart.js"></script> </body>
最后build里的为demo代码用babel编译以后的es5文件
在全局之中有Redux这个对象,取其中的几个属性来用
let {Component} = React; let {render} = ReactDOM; let {createStore, combineReducers} = Redux;
3.1 Redux须要一个store来存放数据
这个store就由createStore建立
3.2 须要定义各个操做是什么,即action
一般来讲它是一个对象,包含type属性表示是什么操做,以及其余属性携带一些数据
它可能长这样子,建议是遵循官方的 一些规范
let upAction = { type: 'UP' };
咱们不止会传type,还会传一些值,若是传不一样的值就let一次就太冗杂了,通常来讲就会用一个方法代替
let upAction = function(value) { return { type: 'up', value }; };
3.3 须要定义怎么处理操做,在redux中它被称做reducer
为何把这种操做称做reducer呢
redux引入了JS数组reduce方法的思想,JS的reduce长这样
var arr = [1, 2, 3, 4]; var num = arr.reduce((a, b) => { return a + b; }); num // 10 var num = arr.reduce((a, b) => { return a + b; }, 5); num // 15
固然了,只是看起来像,实际上差异挺大的,redux的reducer看起来像这样
let upReducer = function(state = 0, action) { switch (action.type) { case 'up': return state + action.value; default: return state; } };
它是一个函数,接收两个参数,第一个参数为数据(即某个状态state),第二个参数为action操做对象
为了切合store中数据与view中视图是一一对应的,reducer规定需始终返回新的state数据,不能直接在原有state中修改;
而且,建议在匹配不到action的时候始终返回默认的state状态,且建议在第一个参数中初始化默认的state值
3.4 在建立store的时候绑定reducer
redux基本上把全部操做都给了store,因此大部分方法都是用store来调用的
其实,你也能够认为Flux中的派发器(dispatcher)就是在里面自动绑定的
let store = createStore(reducer); // let store = createStore(reducer, 10);
如上,建立store的时候传入reducer,能够接收第二个参数表示reducer使用的默认值
3.5 视图发出action动做
在某个时刻,发出了这些动做
store.dispatch(upAction(10));
store.dispatch(upAction(100));
3.6 使用store.getState()获取store中的数据
3.7 动做发出后,reducer匹配动做更新store中的数据,视图view层使用subscribe监听数据的改变
store.subscribe(() => console.log(store.getState()));
来看一下完整的代码
let {Component} = React; let {render} = ReactDOM; let {createStore, combineReducers} = Redux; let upAction = function(value) { return { type: 'up', value }; } let upReducer = function(state = 0, action) { switch (action.type) { case 'up': return state + action.value; default: return state; } }; let store = createStore(upReducer, window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__()); console.log(store.getState()); store.subscribe(() => console.log(store.getState())); store.dispatch(upAction(10)); store.dispatch(upAction(100));
注意上面createStore中第二个参数是用于Redux DevTool的配置,即这个东西
使用这个工具能够便于开发
看看上面代码的输出
初始获取到的值为0,两次action后分别更新相关的数据状态。若是加上初始默认值10
let store = createStore(upReducer, 10, window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__());
3.8 使用多个reducer时,使用Redux的combineReducers方法
action固然不会只是up,多是down,这时能够直接用switch语句切换;但若是action不是这里增减的操做,放在一块儿就有点乱套了
因此须要定义多个reducer,但createStore方法只接收一个reducer,因此就须要整合多个reducer为一个,再统一传入
它看起来像这样
let reducer = combineReducers({upReducer, downReducer}); // let reducer = combineReducers({ // upReducer: upReducer, // downReducer: downReducer // });
接收一个reducer组成的对象,属性表示该reducer对应的state名(如state.upReducer),值表示这个reducer
固然,这个方法咱们能够本身定义,看起来是这样
let myCombineReducers = function(reducerObj) { let newState = {}; return function(state = {}, action) { for (let item in reducerObj) { newState[item] = reducerObj[item](state[item], action); } return newState; } };
其实就是遍历reducer组,返回一个统一的新的reducer,且新的reducer中返回一个新的state
看看Redux中的实现,完整多了
1 function combineReducers(reducers) { 2 var reducerKeys = Object.keys(reducers); 3 var finalReducers = {}; 4 for (var i = 0; i < reducerKeys.length; i++) { 5 var key = reducerKeys[i]; 6 7 if (true) { 8 if (typeof reducers[key] === 'undefined') { 9 (0, _warning2['default'])('No reducer provided for key "' + key + '"'); 10 } 11 } 12 13 if (typeof reducers[key] === 'function') { 14 finalReducers[key] = reducers[key]; 15 } 16 } 17 var finalReducerKeys = Object.keys(finalReducers); 18 19 if (true) { 20 var unexpectedKeyCache = {}; 21 } 22 23 var sanityError; 24 try { 25 assertReducerSanity(finalReducers); 26 } catch (e) { 27 sanityError = e; 28 } 29 30 return function combination() { 31 var state = arguments.length <= 0 || arguments[0] === undefined ? {} : arguments[0]; 32 var action = arguments[1]; 33 34 if (sanityError) { 35 throw sanityError; 36 } 37 38 if (true) { 39 var warningMessage = getUnexpectedStateShapeWarningMessage(state, finalReducers, action, unexpectedKeyCache); 40 if (warningMessage) { 41 (0, _warning2['default'])(warningMessage); 42 } 43 } 44 45 var hasChanged = false; 46 var nextState = {}; 47 for (var i = 0; i < finalReducerKeys.length; i++) { 48 var key = finalReducerKeys[i]; 49 var reducer = finalReducers[key]; 50 var previousStateForKey = state[key]; 51 var nextStateForKey = reducer(previousStateForKey, action); 52 if (typeof nextStateForKey === 'undefined') { 53 var errorMessage = getUndefinedStateErrorMessage(key, action); 54 throw new Error(errorMessage); 55 } 56 nextState[key] = nextStateForKey; 57 hasChanged = hasChanged || nextStateForKey !== previousStateForKey; 58 } 59 return hasChanged ? nextState : state; 60 }; 61 }
加上个down操做,来看看完整代码
let {Component} = React; let {render} = ReactDOM; let {createStore, combineReducers} = Redux; let upAction = function(value) { return { type: 'up', value }; } let downAction = function(value) { return { type: 'down', value }; } let upReducer = function(state = 0, action) { switch (action.type) { case 'up': return state + action.value; default: return state; } }; let downReducer = function(state = 0, action) { switch (action.type) { case 'down': return state - action.value; default: return state; } }; let reducer = combineReducers({upReducer, downReducer}); let store = createStore(reducer, window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__()); console.log(store.getState()); store.subscribe(() => console.log(store.getState())); store.dispatch(upAction(10)); store.dispatch(upAction(100)); store.dispatch(downAction(10)); store.dispatch(downAction(100));
给reducer设个初始值,要注意的是,这个初始值是针对整个state的
若是只有一个reducer,那reducer函数中的state就是这个state
若是用combineReducer整理了多个reducer,那各个reducer函数中的state是整个state中的reducer同名属性的值
let reducer = combineReducers({upReducer, downReducer}); let store = createStore( reducer, {upReducer: 10, downReducer: 10}, window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__() );
如上代码定义了初始值,看看执行结果
Redux是一个独立的技术方案,咱们将它运用到React项目中
接下来的问题主要有三个:
直接点,就是在React组件中调用Redux的subscribe方法来监听同步数据,再在某个时机调用dispatch便可
但官方并不建议使用subscribe这个方法,而是建议使用封装好的另外一个库 React-Redux
与引入Redux相似,你可使用Webpack引入包或浏览器直接引入这个库
而后在全局window下能够获取到这个对象,取一些用到的属性如
let {Provider, connect} = ReactRedux;
class Increase extends Component { constructor(props) { super(props); } componentWillReceiveProps(nextProps) { console.log(nextProps); } increase() { let {dispatch} = this.props; dispatch({ type: 'up' }); } render() { return <p onClick={this.increase.bind(this)}>increase: {this.props.number}</p> } }
组件定义了一个action,即点一次执行一次增加(increase)函数,里面调用dispatch方法发出action,先看看其余东西
function couterUp(state = {number: 100}, action) { switch (action.type) { case 'up': return { number: state.number + 1 }; default: return state; } }
let store = createStore(couterUp, window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__());
要将Redux中的数据同步给React,须要用到这个方法
它看起来像是这样子
let APP = connect(
mapStateToProps,
mapDispatchToProps
)(Increase);
能够把它当作是一个中间件,首先接收几个参数完成配置阶段,而后传入React组件,包装成一个新的东东(它并无直接修改Increase组件)
而通常来讲,通常来讲会传入两个参数(支持四个参数),顾名思义:
第一个参数(类型为函数)
若是不传或置入undefined或null,则表示不须要进行数据更新;不然表示将store中的数据经过props的形式传给React组件
第二个参数(类型为函数)
若是不传或置入undefined或null,则表示将React-Redux中默认的dispatch方法传给React组件;不然表示将redux中的dispatch发出动做经过props的形式传给React组件
注意到上面的React组件代码中,经过props获取到了dispatch方法,而后自行发出动做
increase() { let {dispatch} = this.props; dispatch({ type: 'up' }); }
若是要这样作,mapDispatchToProps 这里就不传入了,即
let APP = connect(
mapStateToProps
)(Increase);
用回常见的方式,在React组件中改一改,直接从props中获取某个dispatch的发出动做
render() { return <p onClick={this.props.increase}>increase: {this.props.number}</p> }
同时修改两个都传入
let APP = connect(
mapStateToProps,
mapDispatchToProps
)(Increase);
咱们定义一下这两个参数(函数),它看起来长这样
function mapStateToProps(state) { return { number: state.number }; } function mapDispatchToProps(dispatch) { return { increase: () => dispatch({ type: 'up' }) }; }
mapStateToProps 中第一个参数为一个对象,表示store中总体的state数据
固然,第一个参数也能够为函数,也能够接收第二个参数,表示自身拥有的属性(ownProps),具体能够看API
最后它返回了一个新的对象,表示要传给React组件的数据
与mapStateToProps相似,mapDispatchToProps 也能够接收两个参数,
第一个表示当前的dispatch方法,第二个表示自身拥有的属性(ownProps)
最后它返回了一个action发出动做(一个函数),传给React组件调用
基本好了,只差一步:将connect包装组件后生成的新东东与实际页面联系起来
使用ReactRedux提供的<Provider />,它看起来是这样
render( <Provider store={store}> <APP /> </Provider>, document.getElementById('box') );
使用store属性传入上面的store对象
在children中置入有connect生成的APP组件,注意这里只能包含一个父层
若是向其中传入属性,如
<APP name="app" />
那么,mapStateToProps中的第二参数ownProps就能够拥有这个name属性
完整代码
let {Component} = React; let {render} = ReactDOM; let {createStore, combineReducers} = Redux; let {Provider, connect} = ReactRedux; class Increase extends Component { constructor(props) { super(props); } componentWillReceiveProps(nextProps) { console.log(nextProps); } render() { return <p onClick={this.props.increase}>increase: {this.props.number}</p> } } function couterUp(state = {number: 100}, action) { switch (action.type) { case 'up': return { number: state.number + 1 }; default: return state; } } function mapStateToProps(state) { return { number: state.number }; } function mapDispatchToProps(dispatch) { return { increase: () => dispatch({ type: 'up' }) }; } let APP = connect( mapStateToProps, mapDispatchToProps )(Increase); let store = createStore(couterUp, window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__()); render( <Provider store={store}> <APP /> </Provider>, document.getElementById('box') );
看一下运行结果
上面说的是单个React组件中的使用,实际使用中会有多个组件
多个组件的使用相似单个,只不过须要注意两点
下面以两个组件的栗子,看看如何实现
4.7.1 首先定义两个组件,一增一减
class Increase extends Component { constructor(props) { super(props); } componentWillReceiveProps(nextProps) { console.log('increase: ', nextProps); } render() { return <p onClick={this.props.increase}>increase: {this.props.number}</p> } } class Decrease extends Component { constructor(props) { super(props); } componentWillReceiveProps(nextProps) { console.log('decrease: ', nextProps); } render() { return <p onClick={this.props.decrease}>decrease: {this.props.number}</p> } }
4.7.2 定义对应的两个reducer
function couterUp(state = {number: 100}, action) { switch (action.type) { case 'up': return { number: state.number + 1 }; default: return state; } } function counterDown(state = {number: -100}, action) { switch (action.type) { case 'down': return { number: state.number - 1 }; default: return state; } }
4.7.3 建立store
let couter = combineReducers({ couterUp, counterDown }); let store = createStore( couter, {couterUp: {number: 10}}, window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__() );
4.7.4 建立链接两个组件对应的两个mapStateToProps 和 mapDispatchToProps
注意state为整个store中的state,取值要取各reducer同名属性如 state.couterUp
function mapStateToProps_1(state) { return { number: state.couterUp.number }; } function mapDispatchToProps_1(dispatch) { return { increase: () => dispatch({ type: 'up' }) }; } function mapStateToProps_2(state, props) { return { number: state.counterDown.number }; } function mapDispatchToProps_2(dispatch) { return { decrease: () => dispatch({ type: 'down' }) }; }
4.7.5 各组件用connect包装
let APP_1 = connect( mapStateToProps_1, mapDispatchToProps_1 )(Increase); let APP_2 = connect( mapStateToProps_2, mapDispatchToProps_2 )(Decrease);
4.7.6 置入<Provider />中
注意只能有一个父级,因此得先简单包装一层
let APP = () => ( <div> <APP_1 /> <APP_2 name="APP_2"/> </div> ); render( <Provider store={store}> <APP /> </Provider>, document.getElementById('box') );
完整代码
1 let {Component} = React; 2 let {render} = ReactDOM; 3 let {createStore, combineReducers} = Redux; 4 let {Provider, connect} = ReactRedux; 5 6 class Increase extends Component { 7 constructor(props) { 8 super(props); 9 } 10 11 componentWillReceiveProps(nextProps) { 12 console.log('increase: ', nextProps); 13 } 14 15 render() { 16 return <p onClick={this.props.increase}>increase: {this.props.number}</p> 17 } 18 } 19 20 class Decrease extends Component { 21 constructor(props) { 22 super(props); 23 } 24 25 componentWillReceiveProps(nextProps) { 26 console.log('decrease: ', nextProps); 27 } 28 29 render() { 30 return <p onClick={this.props.decrease}>decrease: {this.props.number}</p> 31 } 32 } 33 34 function couterUp(state = {number: 100}, action) { 35 switch (action.type) { 36 case 'up': 37 return { 38 number: state.number + 1 39 }; 40 default: 41 return state; 42 } 43 } 44 45 function counterDown(state = {number: -100}, action) { 46 switch (action.type) { 47 case 'down': 48 return { 49 number: state.number - 1 50 }; 51 default: 52 return state; 53 } 54 } 55 56 function mapStateToProps_1(state) { 57 return { 58 number: state.couterUp.number 59 }; 60 } 61 62 function mapDispatchToProps_1(dispatch) { 63 return { 64 increase: () => dispatch({ 65 type: 'up' 66 }) 67 }; 68 } 69 70 function mapStateToProps_2(state, props) { 71 return { 72 number: state.counterDown.number 73 }; 74 } 75 76 function mapDispatchToProps_2(dispatch) { 77 return { 78 decrease: () => dispatch({ 79 type: 'down' 80 }) 81 }; 82 } 83 84 let couter = combineReducers({ 85 couterUp, 86 counterDown 87 }); 88 89 let store = createStore( 90 couter, 91 {couterUp: {number: 10}}, 92 window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__() 93 ); 94 95 96 let APP_1 = connect( 97 mapStateToProps_1, 98 mapDispatchToProps_1 99 )(Increase); 100 101 let APP_2 = connect( 102 mapStateToProps_2, 103 mapDispatchToProps_2 104 )(Decrease); 105 106 let APP = () => ( 107 <div> 108 <APP_1 /> 109 <APP_2 name="APP_2"/> 110 </div> 111 ); 112 113 render( 114 <Provider store={store}> 115 <APP /> 116 </Provider>, 117 document.getElementById('box') 118 );
Good ! 完成了,看看结果
4.7.7 再看connect方法剩余的两个参数
connect方法接收可接收四个参数,上面已经谈到了前两个,后两个不那么经常使用
第三个参数,这里很少说:[mergeProps(stateProps, dispatchProps, ownProps): props
] (Function)
第四个参数:[options
] (Object)
这个options中有以下几个属性:
来看个例子,如今要手动的定义这个参数
针对Decrease,在减1时直接返回了false
let APP_2 = connect( mapStateToProps_2, mapDispatchToProps_2, null, { pure: true, areStatesEqual: (next, prev) => { console.log(next.counterDown, prev.counterDown); return next.counterDown.number < prev.counterDown.number; } } )(Decrease);
能够看到,减1的操做并无传给Decrease组件,页面没有更新
顺便看看有connect包装后的组件
因Redux中操做的执行是同步的,若是要实现异步,好比某个操做用来发个异步请求获取数据,就得引入中间件来处理这种特殊的操做
即这个操做再也不是普通的值,而是一个函数(如Promise异步),经过中间件的处理,让Redux可以解析
先修改上面的栗子,在Increase组件中再也不是每次增长1,而是根据action中的value来指定,好比
function mapDispatchToProps_1(dispatch) { return { increase: () => dispatch({ type: 'up', value: 10 }) }; }
function couterUp(state = {number: 100}, action) { switch (action.type) { case 'up': return { // number: state.number + 1 number: state.number + action.value }; default: return state; } }
这里定义了value是10,但假如value的值得由一个异步的请求才得出呢,要如何放进去
使用Redux提供的中间件applyMiddleware
let {createStore, combineReducers, applyMiddleware} = Redux;
这只是基础的中间件apply函数,它帮助Redux将中间件包装
如今来模拟一个异步请求
function mapDispatchToProps_1(dispatch) { return { // increase: () => dispatch({ // type: 'up', // value: 10 // }) increase: () => dispatch(fetchIncreaseValue('redux-ajaxTest.php')) }; }
可一看到,dispatch中的action是一个函数(这个调用返回的仍是一个函数),而Redux默认只支持对象格式的action,因此这样会报错
这里的fetchIncreaseValue看起来像这样
function fetchIncreaseValue(url) { return function(dispatch) { return $.get(url).then(re => { re = JSON.parse(re); console.log(re); dispatch({ type: 'up', value: re.value }); }) } }
而请求后台后返回值
<?php echo json_encode(array('value' => 100)); ?>
能够看到,异步获取数据以后才执行dispatch发出操做,这里须要一个dispatch关键字
为了拿到这个关键字,得和thunkMiddleware搭配使用(让这个方法可以在内层函数中使用),固然,你也能够再搭配其余中间件
若是使用Webpack打包,就安装好 redux-thunk 包再 import 进来
这里直接引入到浏览器中,引入这个库,而后直接使用(注意这里没有 {} )
let thunkMiddleware = window.ReduxThunk.default;
而后在建立store的时候,传给redux的applyMiddleware便可
let store = createStore( couter, {couterUp: {number: 10}}, applyMiddleware(thunkMiddleware), window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__() );
官方给的例子太复杂了,不过仍是去看看吧,我这里抽出了主要的部分,
先来看看结果
使用这个Redux Dev Tool就得在createStore中配上最后一个参数,而createStore自身的某个参数又能给reducer设置初始值,且applyMiddleware也是在参数中定义
因此要注意的是:
若是用了这个Redux Dev Tool,就要保证applyMiddleware在第三个参数
let store = createStore( couter,
// {}, applyMiddleware(thunkMiddleware), window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__() );
相似这样省略第二个初始值参数,是会报错的
把注释去掉,放上一个空的初始便可,或者不用这个Dev Tool
let store = createStore(
couter,
applyMiddleware(thunkMiddleware)
);
能够去看看其余的Dev Tool