Redux是一個可預測的狀態容器,不但融合了函數式編程思想,還嚴格遵循了單向數據流的理念。Redux繼承了Flux的架構思想,並在此基礎上進行了精簡、優化和擴展,力求用最少的API完成最主要的功能,它的核心代碼短小而精悍,壓縮後只有幾KB。Redux約定了一系列的規範,而且標準化了狀態(即數據)的更新步驟,從而讓不斷變化、快速增加的大型前端應用中的狀態有跡可循,既利於問題的重現,也便於新需求的整合。注意,Redux是一個獨立的庫,可與React、Ember或jQuery等其它庫搭配使用。html
在Redux中,狀態是不能直接被修改的,而是經過Action、Reducer和Store三部分協做完成的。具體的運做流程可簡單的歸納爲三步,首先由Action說明要執行的動做,而後讓Reducer設計狀態的運算邏輯,最後經過Store將Action和Reducer關聯並觸發狀態的更新,下面用代碼演示這個流程。前端
function caculate(previousState = {digit: 0}, action) { //Reducer let state = Object.assign({}, previousState); switch (action.type) { case "ADD": state.digit += 1; break; case "MINUS": state.digit -= 1; } return state; } let store = createStore(caculate); //Store let action = { type: "ADD" }; //Action store.dispatch(action); //觸發更新 store.getState(); //讀取狀態
經過上面的代碼可知,Action是一個普通的JavaScript對象,Reducer是一個純函數,Store是一個經過createStore()函數獲得的對象,若是要觸發狀態的更新,那麼須要調用它的dispatch()方法。先對Redux有個初步的感性認識,而後在接下來的章節中,將圍繞這段代碼展開具體的分析。react
只有遵照Redux所設計的三大原則,才能讓狀態變得可預測。git
(1)單一數據源(Single source of truth)。編程
前端應用中的全部狀態會組成一個樹形的JavaScript對象,被保存到一個Store中。這樣不但能避免數據冗餘,還易於調試,而且便於監控任意時刻的狀態,從而減小出錯機率。不只如此,過去難以達成的功能(例如即時保存、撤銷重作等),如今實現起來也變得易如反掌了。在應用的任意位置,可經過Store的getState()方法讀取到當前的狀態。redux
(2)保持狀態只讀(State is read-only)。數組
若要改變Redux中的狀態,得先派發一個Action對象,而後再由Reducer函數建立一個新的狀態對象返回給Redux,以此保證狀態的只讀,從而讓狀態管理可以井井有理的進行。架構
(3)狀態的改變由純函數完成(Changes are made with pure functions)。app
這裏所說的純函數是指Reducer,它沒有反作用(即輸出可預測),其功能就是接收Action並處理狀態的變動,經過Reducer函數使得歷史狀態變得可追蹤。ide
Redux主要由三部分組成:Action、Reducer和Store,本節將會對它們依次進行講解。
1)Action
由開發者定義的Action本質上就是一個普通的JavaScript對象,Redux約定該對象必須包含一個字符串類型的type屬性,其值是一個常量,用來描述動做意圖。Action的結構可自定義,儘可能包含與狀態變動有關的信息,如下面遞增數值的Action對象爲例,除了必需的type屬性以外,還額外附帶了一個表示增量的step屬性。
{ type: "ADD", step: 1 }
若是項目規模愈來愈大,那麼能夠考慮爲Action加個惟一號標識或者分散到不一樣的文件中。
一般會用Action建立函數(Action Creator)生成Action對象(即返回一個Action對象),由於函數有更好的可控性、移植性和可測試性,下面是一個簡易的Action建立函數。
function add() { return { type: "ADD", step: 1 }; }
2)Reducer
Reducer函數對狀態只計算不存儲,開發者可根據當前業務對其進行自定義。此函數能接收2個參數:previousState和action,前者表示上一個狀態(即當前應用的狀態),後者是一個被派發的Action對象,函數體中的返回值是根據這兩個參數生成的一個處理過的新狀態。
Redux在首次執行時,因爲初始狀態爲undefined,所以能夠爲previousState設置初始值,例如像下面這樣使用ES6默認參數的語法。
function caculate(previousState = {digit: 0}, action) { let state = Object.assign({}, previousState); //省略更新邏輯 return state; }
在編寫Reducer函數時,有三點須要注意:
(1)遵照純函數的規範,例如不修改參數、不執行有反作用的函數等。
(2)在函數中能夠先用Object.assign()建立一個狀態對象的副本,隨後就只修改這個新對象,注意,方法的第一個參數要像上面這樣傳一個空對象。
(3)在發生異常狀況(例如沒法識別傳入的Action對象),返回原來的狀態。
當業務變得複雜時,Reducer函數中處理狀態的邏輯也會隨之變得異常龐大。此時,就能夠採用分而治之的設計思想,將其拆分紅一個個小型的獨立子函數,而這些Reducer函數各自只負責維護一部分狀態。若是須要將它們合併成一個完整的Reducer函數,那麼可使用Redux提供的combineReducers()函數。該函數會接收一個由拆分的Reducer函數組成的對象,而且能將它們的結果合併成一個完整的狀態對象。下面是一個用法示例,先將以前的caculate()函數拆分紅add()和minus()兩個函數,再做爲參數傳給combineReducers()函數。
function add(previousState, action) { let state = Object.assign({}, previousState); state.digit = "digit" in state ? (state.digit + 1) : 0; return state; } function minus(previousState, action) { let state = Object.assign({}, previousState); state.number = "number" in state ? (state.number - 1) : 0; return state; } let reducers = combineReducers({add, minus});
combineReducers()會先執行一次這兩個函數,也就是說reducers()函數所要計算的初始狀態再也不是undefined,而是下面這個對象。注意,{add, minus}用到了ES6新增的簡潔屬性語法。
{ add: { digit: 0 }, minus: { number: 0 } }
3)Store
Store爲Action和Reducer架起了一座溝通的橋樑,它是Redux中的一個對象,發揮了容器的做用,保存着應用的狀態,包含4個方法:
(1)getState():獲取當前狀態。
(2)dispatch(action):派發一個Action對象,引發狀態的修改。
(3)subscribe(listener):註冊狀態更新的監聽器,其返回值能夠註銷該監聽器。
(4)replaceReducer(nextReducer):更新Store中的Reducer函數,在實現Redux熱加載時可能會用到。
在Redux應用中,只會包含一個Store,由createStore()函數建立,它的第一個參數是Reducer()函數,第二個參數是可選的初始狀態,以下代碼所示,爲其傳入了開篇的caculate()函數和一個包含digit屬性的對象。
let store = createStore(caculate, {digit: 1});
caculate()函數會增長或減小狀態對象的digit屬性,其中增量或減量都是1。接下來爲Store註冊一個監聽器(以下代碼所示),當狀態更新時,就會打印出最新的狀態;而在註銷監聽器(即調用unsubscribe()函數)後,控制檯就不會再有任何輸出。
let unsubscribe = store.subscribe(() => //註冊監聽器 console.log(store.getState()) ); store.dispatch({ type: "ADD" }); //{digit: 2} store.dispatch({ type: "ADD" }); //{digit: 3} unsubscribe(); //註銷監聽器 store.dispatch({ type: "MINUS" }); //沒有輸出
雖然Redux和React能夠單獨使用(即沒有直接關聯),可是將二者搭配起來能發揮更大的做用。React應用的規模一旦上去,那麼對狀態的維護就變得越發棘手,而在引入Redux後就能規範狀態的變化,從而扭轉這種窘境。Redux官方提供了一個用於綁定React的庫:react-redux,它包含一個connect()函數和一個Provider組件,能很方便的將Redux的特性融合到React組件中。
1)容器組件和展現組件
因爲react-redux庫是基於容器組件和展現組件相分離的開發思想而設計的,所以在正式講解react-redux以前,須要先理清這兩類組件的概念。
容器組件(Container Component),也叫智能組件(Smart Component),由react-redux庫生成,負責應用邏輯和源數據的處理,爲展現組件傳遞必要的props,可與Redux配合使用,不只能監聽Redux的狀態變化,還能向Redux派發Action。
展現組件(Presentational Component),也叫木偶組件(Dumb Component),由開發者定義,負責渲染界面,接收從容器組件傳來的props,可經過props中的回調函數同步源數據的變動。
容器組件和展現組件是根據職責劃分的,二者可互相嵌套,而且它們內部均可以包含或省略狀態,通常容器組件是一個有狀態的類,而展現組件是一個無狀態的函數。
2)connect()
react-redux提供了一個柯里化函數:connect(),它包含4個可選的參數(以下代碼所示),用於鏈接React組件與Redux的Store(即讓展現組件關聯Redux),生成一個容器組件。
connect([mapStateToProps], [mapDispatchToProps], [mergeProps], [options])
在使用connect()時會有兩次函數執行,以下代碼所示,第一次是獲取要使用的保存在Store中的狀態,connect()函數的返回結果是一個函數;第二次是把一個展現組件Dumb傳到剛剛返回的函數中,繼而將該組件裝飾成一個容器組件Smart。
const Smart = connect()(Dumb);
接下來會着重講解函數的前兩個參數:mapStateToProps和mapDispatchToProps,另外兩個參數(mergeProps和options)能夠參考官方文檔的說明。
3)mapStateToProps
這是一個包含2個參數的函數(以下代碼所示),其做用是從Redux的Store中提取出所需的狀態並計算成展現組件的props。若是connect()函數省略這個參數,那麼展現組件將沒法監聽Store的變化。
mapStateToProps(state, [ownProps])
第一個state參數是Store中保存的狀態,第二個可選的ownProps參數是傳遞給容器組件的props對象。在通常狀況下,mapStateToProps()函數會返回一個對象,但當須要控制渲染性能時,能夠返回一個函數。下面是一個簡單的例子,仍是沿用開篇的caculate()函數,Provider組件的功能將在後文中講解。
let store = createStore(caculate); function Btn(props) { //展現組件 return <button>{props.txt}</button>; } function mapStateToProps(state, ownProps) { console.log(state); //{digit: 0} console.log(ownProps); //{txt: "提交"} return state; } let Smart = connect(mapStateToProps)(Btn); //生成容器組件 ReactDOM.render( <Provider store={store}> <Smart txt="提交" /> </Provider>, document.getElementById("container") );
Btn是一個無狀態的展現組件,Store中保存的初始狀態不是undefined,容器組件Smart接收到了一個txt屬性,在mapStateToProps()函數中打印出了兩個參數的值。
當Store中的狀態發生變化或組件接收到新的props時,mapStateToProps()函數就會被自動調用。
4)mapDispatchToProps
它既能夠是一個對象,也能夠是一個函數,以下代碼所示。其做用是綁定Action建立函數與Store實例所提供的dispatch()方法,再將綁好的方法映射到展現組件的props中。
function add() { //Action建立函數 return {type: "ADD"}; } var mapDispatchToProps = { add }; //對象 var mapDispatchToProps = (dispatch, ownProps) => { //函數 return {add: bindActionCreators(add, dispatch)}; }
當mapDispatchToProps是一個對象時,其包含的方法會做爲Action建立函數,自動傳遞給Redux內置的bindActionCreators()方法,生成的新方法會合併到props中,屬性名沿用以前的方法名。
當mapDispatchToProps是一個函數時,會包含2個參數,第一個dispatch參數就是Store實例的dispatch()方法;第二個ownProps參數的含義與mapStateToProps中的相同,而且也是可選的。函數的返回值是一個由方法組成的對象(會合併到props中),在方法中會派發一個Action對象,而利用bindActionCreators()方法就能簡化派發流程,其源碼以下所示。
function bindActionCreator(actionCreator, dispatch) { return function () { return dispatch(actionCreator.apply(this, arguments)); }; }
展現組件能經過讀取props的屬性來調用傳遞過來的方法,例如在Btn組件的點擊事件中執行props.add(),觸發狀態的更新,以下所示。
function Btn(props) { return <button onClick={props.add}>{props.txt}</button>; }
經過上面的分析可知,mapStateToProps負責展現組件的輸入,即將所需的應用狀態映射到props中;mapDispatchToProps負責展現組件的輸出,即將須要執行的更新操做映射到props中。
5)Provider
react-redux提供了Provider組件,它能將Store保存在本身的Context(在第9篇作過講解)中。若是要正確使用容器組件,那麼得讓其成爲Provider組件的後代,而且只有這樣才能接收到傳遞過來的Store。Provider組件常見的用法以下所示。
<Provider store={store}> <Smart /> </Provider>
Provider組件位於頂層的位置,它會接收一個store屬性,屬性值就是createStore()函數的返回值,Smart是一個容器組件,被嵌套在Provider組件中。