本文的目的很簡單,介紹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