Redux與它的中間件:redux-thunk,redux-actions,redux-promise,redux-saga


序言

這里要講的就是一個Redux在React中的應用問題,講一講Redux,react-redux,redux-thunk,redux-actions,redux-promise,redux-saga這些包的作用和他們解決的問題。
因為不想把篇幅拉得太長,所以沒有太多源碼分析和語法講解,能怎么簡單就怎么簡單。

Redux

先看看百度百科上面Redux的一張圖:

這是Redux在Github上的介紹:Redux用於js程序,是一個可預測的狀態容器。

在這里我們首先要明白的是什么叫可預測?什么叫狀態容器?

什么叫狀態?實際上就是變量,對話框顯示或隱藏的變量,一杯奶茶多少錢的變量。

那么這個狀態容器,實際上就是一個存放這些變量的變量。

你創建了一個全局變量叫Store,然后將代碼中控制各個狀態的變量存放在里面,那么現在Store就叫做狀態容器。

什么叫可預測?

你在操作這個Store的時候,總是用Store.price的方式來設置值,這種操作數據的方式很原始,對於復雜的系統而言永遠都不知道程序在運行的過程中發生了什么。

那么現在我們都通過發送一個Action去做修改,而Store在接收到Action后會使用Reducer對Action傳遞的數據做處理,最后應用到Store中。

相對於Store.price的方式來修改者,這種方式無疑更麻煩,但是這種方式的好處就是,每一個Action里面都可以寫日志,可以記錄各種狀態的變動,這就是可預測。

所以如果你的程序很簡單,你完全沒有必要去用Redux。

看看Redux的示例代碼:

actionTypes.js:

export const CHANGE_BTN_TEXT = 'CHANGE_BTN_TEXT';

actions.js:

import * as T from './actionTypes';

export const changeBtnText = (text) => {
  return {
    type: T.CHANGE_BTN_TEXT,
    payload: text
  };
};

reducers.js:

import * as T from './actionTypes';

const initialState = {
  btnText: '我是按鈕',
};

const pageMainReducer = (state = initialState, action) => {
  switch (action.type) {
    case T.CHANGE_BTN_TEXT:
      return {
        ...state,
        btnText: action.payload
      };
    default:
      return state;
  }
};

export default pageMainReducer;

index.js

import { createStore } from 'redux';
import reducer from './reducers';
import { changeBtnText } from './actions';

const store = createStore(reducer);
// 開始監聽,每次state更新,那么就會打印出當前狀態
const unsubscribe = store.subscribe(() => {
  console.info(store.getState());
});
// 發送消息
store.dispatch(changeBtnText('點擊了按鈕'));
// 停止監聽state的更新
unsubscribe();

這里就不解釋什么語法作用了,網上這樣的資料太多了。

Redux與React的結合:react-redux

Redux是一個可預測的狀態容器,跟React這種構建UI的庫是兩個相互獨立的東西。

Redux要應用到React中,很明顯action,reducer,dispatch這幾個階段並不需要改變,唯一需要考慮的是redux中的狀態需要如何傳遞給react組件。

很簡單,只需要每次要更新數據時運用store.getState獲取到當前狀態,並將這些數據傳遞給組件即可。

那么問題來了,如何讓每個組件都獲取到store呢?

當然是將store作為一個值傳遞給根組件,然后store就會一級一級往下傳,使得每個組件都能獲取到store的值。

但是這樣太繁瑣了,難道每個組件需要寫一個傳遞store的邏輯?為了解決這個問題,那么得用到React的context玩法,通過在根組件上將store放在根組件的context中,然后在子組件中通過context獲取到store。

react-redux的主要思路也是如此,通過嵌套組件Provider將store放到context中,通過connect這個高階組件,來隱藏取store的操作,這樣我們就不需要每次去操作context寫一大堆代碼那么麻煩了。

然后我們再來基於之前的Redux示例代碼給出react-redux的使用演示代碼,其中action和reduce部分不變,先增加一個組件PageMain:

const PageMain = (props) => {
  return (
    <div>
      <button onClick={() => {
        props.changeText('按鈕被點擊了');
      }}
      >
        {props.btnText}
      </button>
    </div>
  );
};
// 映射store.getState()的數據到PageMain
const mapStateToProps = (state) => {
  return {
    btnText: state.pageMain.btnText,
  };
};
// 映射使用了store.dispatch的函數到PageMain
const mapDispatchToProps = (dispatch) => {
  return {
    changeText: (text) => {
      dispatch(changeBtnText(text));
    }
  };
};

// 這個地方也可以簡寫,react-redux會自動做處理
const mapDispatchToProps = {
  changeText: changeBtnText
};

export default connect(mapStateToProps, mapDispatchToProps)(PageMain);

注意上面的state.pageMain.btnText,這個pageMain是我用redux的combineReducers將多個reducer合並后給的原先的reducer一個命名。

它的代碼如下:

import { combineReducers } from 'redux';
import pageMain from './components/pageMain/reducers';

const reducer = combineReducers({
  pageMain
});

export default reducer;

然后修改index.js:

import React from 'react';
import { createStore } from 'redux';
import { Provider } from 'react-redux';
import ReactDOM from 'react-dom';
import reducer from './reducers';
import PageMain from './components/pageMain';

const store = createStore(reducer);

const App = () => (
  <Provider store={store}>
    <PageMain />
  </Provider>
);

ReactDOM.render(<App />, document.getElementById('app'));

Redux的中間件

之前我們講到Redux是個可預測的狀態容器,這個可預測在於對數據的每一次修改都可以進行相應的處理和記錄。

假如現在我們需要在每次修改數據時,記錄修改的內容,我們可以在每一個dispatch前面加上一個console.info記錄修改的內容。

但是這樣太繁瑣了,所以我們可以直接修改store.dispatch:

let next = store.dispatch
store.dispatch = (action)=> {
  console.info('修改內容為:', action)
  next(action)
}

Redux中也有同樣的功能,那就是applyMiddleware。直譯過來就是“應用中間件”,它的作用就是改造dispatch函數,跟上面的玩法基本雷同。

來一段演示代碼:

import { createStore, applyMiddleware } from 'redux';
import reducer from './reducers';

const store = createStore(reducer, applyMiddleware(curStore => next => action => {
  console.info(curStore.getState(), action);
  return next(action);
}));

看起來挺奇怪的玩法,但是理解起來並不難。通過這種返回函數的方法,使得applyMiddleware內部以及我們使用時可以處理store和action,並且這里next的應用就是為了使用多個中間件而存在的。

而通常我們沒有必要自己寫中間件,比如日志的記錄就已經有了成熟的中間件:redux-logger,這里給一個簡單的例子:

import { applyMiddleware, createStore } from 'redux';
import createLogger from 'redux-logger';
import reducer from './reducers';

const logger = createLogger();

const store = createStore(
  reducer,
  applyMiddleware(logger)
);

這樣就可以記錄所有action及其發送前后的state的日志,我們可以了解到代碼實際運行時到底發生了什么。

redux-thunk:處理異步action

在上面的代碼中,我們點擊按鈕后,直接修改了按鈕的文本,這個文本是個固定的值。

actions.js:

import * as T from './actionTypes';

export const changeBtnText = (text) => {
  return {
    type: T.CHANGE_BTN_TEXT,
    payload: text
  };
};

但是在我們實際生產的過程中,很多情況都是需要去請求服務端拿到數據再修改的,這個過程是一個異步的過程。又或者需要setTimeout去做一些事情。

我們可以去修改這一部分如下:

const mapDispatchToProps = (dispatch) => {
  return {
    changeText: (text) => {
      dispatch(changeBtnText('正在加載中'));
      axios.get('http://test.com').then(() => {
        dispatch(changeBtnText('加載完畢'));
      }).catch(() => {
        dispatch(changeBtnText('加載有誤'));
      });
    }
  };
};

實際上,我們每天不知道要處理多少這樣的代碼。

但是問題來了,異步操作相比同步操作多了一個很多確定因素,比如我們展示正在加載中時,可能要先要做異步操作A,而請求后台的過程卻非常快,導致加載完畢先出現,而這時候操作A才做完,然后再展示加載中。

所以上面的這個玩法並不能滿足這種情況。

這個時候我們需要去通過store.getState獲取當前狀態,從而判斷到底是展示正在加載中還是展示加載完畢。

這個過程就不能放在mapDispatchToProps中了,而需要放在中間件中,因為中間件中可以拿到store。

首先創造store的時候需要應用react-thunk,也就是

import { createStore, applyMiddleware } from 'redux';
import thunk from 'redux-thunk';
import reducer from './reducers';

const store = createStore(
  reducer,
  applyMiddleware(thunk)
);

它的源碼超級簡單:

function createThunkMiddleware(extraArgument) {
  return ({ dispatch, getState }) => next => action => {
    if (typeof action === 'function') {
      return action(dispatch, getState, extraArgument);
    }
    return next(action);
  };
}

const thunk = createThunkMiddleware();
thunk.withExtraArgument = createThunkMiddleware;

export default thunk;

從這個里面可以看出,它就是加強了dispatch的功能,在dispatch一個action之前,去判斷action是否是一個函數,如果是函數,那么就執行這個函數。

那么我們使用起來就很簡單了,此時我們修改actions.js

import axios from 'axios';
import * as T from './actionTypes';

export const changeBtnText = (text) => {
  return {
    type: T.CHANGE_BTN_TEXT,
    payload: text
  };
};

export const changeBtnTextAsync = (text) => {
  return (dispatch, getState) => {
    if (!getState().isLoading) {
      dispatch(changeBtnText('正在加載中'));
    }
    axios.get(`http://test.com/${text}`).then(() => {
      if (getState().isLoading) {
        dispatch(changeBtnText('加載完畢'));
      }
    }).catch(() => {
      dispatch(changeBtnText('加載有誤'));
    });
  };
};

而原來mapDispatchToProps中的玩法和同步action的玩法是一樣的:

const mapDispatchToProps = (dispatch) => {
  return {
    changeText: (text) => {
      dispatch(changeBtnTextAsync(text));
    }
  };
};

通過redux-thunk我們可以簡單地進行異步操作,並且可以獲取到各個異步操作時期狀態的值。

redux-actions:簡化redux的使用

Redux雖然好用,但是里面還是有些重復代碼,所以有了redux-actions來簡化那些重復代碼。

這部分簡化工作主要集中在構造action和處理reducers方面。

先來看看原先的actions

import axios from 'axios';
import * as T from './actionTypes';

export const changeBtnText = (text) => {
  return {
    type: T.CHANGE_BTN_TEXT,
    payload: text
  };
};

export const changeBtnTextAsync = () => {
  return (dispatch, getState) => {
    if (!getState().isLoading) {
      dispatch(changeBtnText('正在加載中'));
    }
    axios.get('http://test.com').then(() => {
      if (getState().isLoading) {
        dispatch(changeBtnText('加載完畢'));
      }
    }).catch(() => {
      dispatch(changeBtnText('加載有誤'));
    });
  };
};

然后再來看看修改后的:

import axios from 'axios';
import * as T from './actionTypes';
import { createAction } from 'redux-actions';

export const changeBtnText = createAction(T.CHANGE_BTN_TEXT, text => text);

export const changeBtnTextAsync = () => {
  return (dispatch, getState) => {
    if (!getState().isLoading) {
      dispatch(changeBtnText('正在加載中'));
    }
    axios.get('http://test.com').then(() => {
      if (getState().isLoading) {
        dispatch(changeBtnText('加載完畢'));
      }
    }).catch(() => {
      dispatch(changeBtnText('加載有誤'));
    });
  };
};

這一塊代碼替換上面的部分代碼后,程序運行結果依然保持不變,也就是說createAction只是對上面的代碼進行了簡單的封裝而已。

這里注意到,異步的action就不要用createAction,因為這個createAction返回的是一個對象,而不是一個函數,就會導致redux-thunk的代碼沒有起到作用。

這里也可以使用createActions這個函數同時創建多個action,但是講道理,這個語法很奇怪,用createAction就好。

同樣redux-actions對reducer的部分也進行了處理,比如handleAction以及handelActions。

先來看看原先的reducers

import * as T from './actionTypes';

const initialState = {
  btnText: '我是按鈕',
};

const pageMainReducer = (state = initialState, action) => {
  switch (action.type) {
    case T.CHANGE_BTN_TEXT:
      return {
        ...state,
        btnText: action.payload
      };
    default:
      return state;
  }
};

export default pageMainReducer;

然后使用handleActions來處理

import { handleActions } from 'redux-actions';
import * as T from './actionTypes';

const initialState = {
  btnText: '我是按鈕',
};

const pageMainReducer = handleActions({
  [T.CHANGE_BTN_TEXT]: {
    next(state, action) {
      return {
        ...state,
        btnText: action.payload,
      };
    },
    throw(state) {
      return state;
    },
  },
}, initialState);

export default pageMainReducer;

這里handleActions可以加入異常處理,並且幫助處理了初始值。

注意,無論是createAction還是handleAction都只是對代碼做了一點簡單的封裝,兩者可以單獨使用,並不是說使用了createAction就必須要用handleAction。

redux-promise:redux-actions的好基友,輕松創建和處理異步action

還記得上面在使用redux-actions的createAction時,我們對異步的action無法處理。

因為我們使用createAction后返回的是一個對象,而不是一個函數,就會導致redux-thunk的代碼沒有起到作用。

而現在我們將使用redux-promise來處理這類情況。

可以看看之前我們使用 createAction的例子:

export const changeBtnText = createAction(T.CHANGE_BTN_TEXT, text => text);

現在我們先加入redux-promise中間件:

import thunk from 'redux-thunk';
import createLogger from 'redux-logger';
import promiseMiddleware from 'redux-promise';
import reducer from './reducers';

const store = createStore(reducer, applyMiddleware(thunk, createLogger, promiseMiddleware));

然后再處理異步action:

export const changeBtnTextAsync = createAction(T.CHANGE_BTN_TEXT_ASYNC, (text) => {
  return axios.get(`http://test.com/${text}`);
});

可以看到我們這里返回的是一個Promise對象.(axios的get方法結果就是Promise對象)

我們還記得redux-thunk中間件,它會去判斷action是否是一個函數,如果是就執行。

而我們這里的redux-promise中間件,他會在dispatch時,判斷如果action不是類似

{
  type:'',
  payload: ''
}

這樣的結構,也就是 FSA,那么就去判斷是否為promise對象,如果是就執行action.then的玩法。

很明顯,我們createAction后的結果是FSA,所以會走下面這個分支,它會去判斷action.payload是否為promise對象,是的話那就

action.payload
  .then(result => dispatch({ ...action, payload: result }))
  .catch(error => {
    dispatch({ ...action, payload: error, error: true });
    return Promise.reject(error);
  })

也就是說我們的代碼最后會轉變為:

axios.get(`http://test.com/${text}`)
  .then(result => dispatch({ ...action, payload: result }))
  .catch(error => {
    dispatch({ ...action, payload: error, error: true });
    return Promise.reject(error);
  })

這個中間件的代碼也很簡單,總共19行,大家可以在github上直接看看。

redux-saga:控制器與更優雅的異步處理

我們的異步處理用的是redux-thunk + redux-actions + redux-promise,其實用起來還是蠻好用的。

但是隨着ES6中Generator的出現,人們發現用Generator處理異步可以更簡單。

而redux-saga就是用Generator來處理異步。

以下講的知識是基於Generator的,如果您對這個不甚了解,可以簡單了解一下相關知識,大概需要2分鍾時間,並不難。

redux-saga文檔並沒有說自己是處理異步的工具,而是說用來處理邊際效應(side effects),這里的邊際效應你可以理解為程序對外部的操作,比如請求后端,比如操作文件。

redux-saga同樣是一個redux中間件,它的定位就是通過集中控制action,起到一個類似於MVC中控制器的效果。

同時它的語法使得復雜異步操作不會像promise那樣出現很多then的情況,更容易進行各類測試。

這個東西有它的好處,同樣也有它不好的地方,那就是比較復雜,有一定的學習成本。

並且我個人而言很不習慣Generator的用法,覺得Promise或者await更好用。

這里還是記錄一下用法,畢竟有很多框架都用到了這個。

應用這個中間件和我們的其他中間件沒有區別:

import React from 'react';
import { createStore, applyMiddleware } from 'redux';
import promiseMiddleware from 'redux-promise';
import createSagaMiddleware from 'redux-saga';
import {watchDelayChangeBtnText} from './sagas';
import reducer from './reducers';

const sagaMiddleware = createSagaMiddleware();

const store = createStore(reducer, applyMiddleware(promiseMiddleware, sagaMiddleware));

sagaMiddleware.run(watchDelayChangeBtnText);

創建saga中間件后,然后再將其中間件接入到store中,最后需要用中間件運行sagas.js返回的Generator,監控各個action。

現在我們給出sagas.js的代碼:

import { delay } from 'redux-saga';
import { put, call, takeEvery } from 'redux-saga/effects';
import * as T from './components/pageMain/actionTypes';
import { changeBtnText } from './components/pageMain/actions';

const consoleMsg = (msg) => {
  console.info(msg);
};
/**
 * 處理編輯效應的函數
 */
export function* delayChangeBtnText() {
  yield delay(1000);
  yield put(changeBtnText('123'));
  yield call(consoleMsg, '完成改變');
}
/**
 * 監控Action的函數
 */
export function* watchDelayChangeBtnText() {
  yield takeEvery(T.WATCH_CHANGE_BTN_TEXT, delayChangeBtnText);
}

在redux-saga中有一類用來處理邊際效應的函數比如put、call,它們的作用是為了簡化操作。

比如put相當於redux的dispatch的作用,而call相當於調用函數。(可以參考上面代碼中的例子)

還有另一類函數就是類似於takeEvery,它的作用就是和普通redux中間件一樣攔截到action后作出相應處理。

比如上面的代碼就是攔截到T.WATCH_CHANGE_BTN_TEXT這個類型的action,然后調用delayChangeBtnText。

然后可以回看我們之前的代碼,有這么一行代碼:

sagaMiddleware.run(watchDelayChangeBtnText);

這里實際就是引入監控的這個生成器后,再運行監控生成器。

這樣我們在代碼里面dispatch類型為T.WATCH_CHANGE_BTN_TEXT的action時就會被攔截然后做出相應處理。

當然這里有人可能會提出疑問,難道每一個異步都要這么寫嗎,那豈不是要run很多次?

當然不是這個樣子,我們可以在saga中這么寫:

export default function* rootSaga() {
  yield [
    watchDelayChangeBtnText(),
    watchOtherAction()
  ]
}

我們只需要按照這個格式去寫,將watchDelayChangeBtnText這樣用於監控action的生成器放在上面那個代碼的數組中,然后作為一個生成器返回。

現在只需要引用這個rootSaga即可,然后run這個rootSaga。

以后如果要監控更多的action,只需要在sagas.js中加上新的監控的生成器即可。

通過這樣的處理,我們就將sagas.js做成了一個像MVC中的控制器的東西,可以用來處理各種各樣的action,處理復雜的異步操作和邊際效應。

但是這里要注意,一定要加以區分sagas.js中使用監控的action和真正功能用的action,比如加個watch關鍵字,以免業務復雜后代碼混亂。

總結

總的來說:

  • redux是一個可預測的狀態容器,
  • react-redux是將store和react結合起來,使得數據展示和修改對於react項目而言更簡單
  • redux中間件就是在dispatch action前對action做一些處理
  • redux-thunk用於對異步做操作
  • redux-actions用於簡化redux操作
  • redux-promise可以配合redux-actions用來處理Promise對象,使得異步操作更簡單
  • redux-saga可以起到一個控制器的作用,集中處理邊際效用,並使得異步操作的寫法更優雅。

OK,雖然說不想寫那么多,結果還是寫了一大堆。

如果您覺得對您還有幫助,那么也請點個贊吧。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM