1. 程式人生 > >React躬行記(11)——Redux基礎

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