React躬行記(11)——Redux基礎
Redux是一個可預測的狀態容器,不但融合了函數語言程式設計思想,還嚴格遵循了單向資料流的理念。Redux繼承了Flux的架構思想,並在此基礎上進行了精簡、優化和擴充套件,力求用最少的API完成最主要的功能,它的核心程式碼短小而精悍,壓縮後只有幾KB。Redux約定了一系列的規範,並且標準化了狀態(即資料)的更新步驟,從而讓不斷變化、快速增長的大型前端應用中的狀態有跡可循,既利於問題的重現,也便於新需求的整合。注意,Redux是一個獨立的庫,可與React、Ember或jQuery等其它庫搭配使用。
在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有個初步的感性認識,然後在接下來的章節中,將圍繞這段程式碼展開具體的分析。
一、三大原則
只有遵守Redux所設計的三大原則,才能讓狀態變得可預測。
(1)單一資料來源(Single source of truth)。
前端應用中的所有狀態會組成一個樹形的JavaScript物件,被儲存到一個Store中。這樣不但能避免資料冗餘,還易於除錯,並且便於監控任意時刻的狀態,從而減少出錯概率。不僅如此,過去難以達成的功能(例如即時儲存、撤銷重做等),現在實現起來也變得易如反掌了。在應用的任意位置,可通過Store的getState()方法讀取到當前的狀態。
(2)保持狀態只讀(State is read-only)。
若要改變Redux中的狀態,得先派發一個Action物件,然後再由Reducer函式建立一個新的狀態物件返回給Redux,以此保證狀態的只讀,從而讓狀態管理能夠井然有序的進行。
(3)狀態的改變由純函式完成(Changes are made with pure functions)。
這裡所說的純函式是指Reducer,它沒有副作用(即輸出可預測),其功能就是接收Action並處理狀態的變更,通過Reducer函式使得歷史狀態變得可追蹤。
二、主要組成
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" }); //沒有輸出
三、繫結React
雖然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元件中。
&n