Redux:Middleware你咋就這麼難

  這段時間都在學習Redux,感受對我來講初學難度很大,中文官方文檔讀了好多遍才大概有點入門的感受,小小地總結一下,首先能夠看一下Redux的基本流程:
此處輸入圖片的描述javascript

  從上面的圖能夠看出,簡單來講,單一的state是存儲在store中,當要對state進行更新的時候,首先要發起一個action(經過dispatch函數),action的做用就是至關於一個消息通知,用來描述發生了什麼(好比:增長一個Todo),而後reducer會根據action來進行對state更新,這樣就能夠根據新的state去渲染View。
  
  固然上面僅僅是發生同步Action的狀況下,若是是Action是異步的(例如從服務器獲取數據),那麼狀況就有所不一樣了,必需要藉助Redux的中間件Middleware。
  java

Redux moddleware provides a third-party extension point between dispatching an action, and the moment it reaches the reducergit

  根據官方的解釋,Redux中間件在發起一個actionaction到達reducer的之間,提供了一個第三方的擴展。本質上經過插件的形式,將本來的action->redux的流程改變爲action->middleware1->middleware2-> ... ->reducer,經過改變數據流,從而實現例如異步Action、日誌輸入的功能。
  首先咱們舉例不使用中間件Middleware建立store:github

import rootReducer from './reducers'
import {createStore} from 'redux'

let store =  createStore(rootReducer);

  那麼使用中間件的狀況下(以redux-logger舉例),建立過程以下:面試

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

const loggerMiddleware = createLogger();
let store = applyMiddleware(loggerMiddleware)(createStore)(rootReducer);

  
  那麼咱們不經要問了,爲何採用了上面的代碼就能夠實現打印日誌的中間件呢?
  首先給出applyMiddleware的源碼(Redux1.0.1版本):編程

export default function applyMiddleware(...middlewares) {            return (next)  => 
        (reducer, initialState) => {

              var store = next(reducer, initialState);
              var dispatch = store.dispatch;
              var chain = [];

              var middlewareAPI = {
                getState: store.getState,
                dispatch: (action) => dispatch(action)
              };

              chain = middlewares.map(middleware =>
                            middleware(middlewareAPI));
              dispatch = compose(...chain, store.dispatch);
              return {
                ...store,
                dispatch
              };
           };
}

  上面的代碼雖然只有不到20行,但看懂確實是不太容易,實際上包含了函數式編程各類技術,首先最明顯的使用到了柯里化(Currying),在我理解中柯里化(Currying)實際就是將多參數函數轉化爲單參數函數並延遲執行函數,例如:json

function add(x){
    return function(y){
        return x + y;
    }
}
var add5 = add(5);
console.log(add5(10)); // 10

  關於柯里化(Currying)更詳細的介紹能夠看我以前的一篇文章從一道面試題談談函數柯里化(Currying)
  首先咱們看applyMiddleware的整體結構:redux

export default function applyMiddleware(...middlewares) {            return (next)  => 
        (reducer, initialState) => {
        };
}

  哈哈,典型的柯里化(Currying),其中(...middlewares)用到了ES6中的新特性,用於將任意箇中間件參數轉化爲中間件數組,所以很容易看出來在該函數的調用方法就是:segmentfault

let store = applyMiddleware(middleware1,middleware2)(createStore)(rootReducer);

  其中applyMiddleware形參和實參的對應關係是:api

形參 實參
middlewares [middleware1,middleware2]
createStore Redux原生createStore
reducer, preloadedState, enhancer 原生createStore須要填入的參數

  再看函數體:

var store = next(reducer, initialState);
var dispatch = store.dispatch;
var chain = [];
var middlewareAPI = {
    getState: store.getState,
    dispatch: (action) => dispatch(action)
};

  上面代碼很是簡單,首先獲得store,並將以前的store.dispatch存儲在變量dispatch中,聲明chain,以及將middleware須要的參數存儲到變量middlewareAPI中。接下來的函數就有點難度了,讓咱們一行一行來看。

chain = middlewares.map(middleware => middleware(middlewareAPI))

  上面實際的含義就是將middleware數組每個middleware執行
middleware(middlewareAPI)的返回值保存的chain數組中。那麼咱們不經要問了,中間件函數究竟是怎樣的?咱們提供一個精簡版的createLogger函數:

export default function createLogger({ getState }) {
      return (next) => 
        (action) => {
              const console = window.console;
              const prevState = getState();
              const returnValue = next(action);
              const nextState = getState();
              const actionType = String(action.type);
              const message = `action ${actionType}`;

              console.log(`%c prev state`, `color: #9E9E9E`, prevState);
              console.log(`%c action`, `color: #03A9F4`, action);
              console.log(`%c next state`, `color: #4CAF50`, nextState);
              return returnValue;
    };
}

  可見中間件createLogger也是典型的柯里化(Currying)函數。{getState}使用了ES6的解構賦值,createLogger(middlewareAPI))返回的(也就是數組chain存儲的是)函數的結構是:

(next) => (action) => {
//包含getState、dispatch函數的閉包
};

  咱們接着看咱們的applyMiddleware函數

dispatch = compose(...chain,store.dispatch)

  這句是最精妙也是最有難度的地方,注意一下,這裏的...操做符是數組展開,下面咱們先給出Redux中複合函數compose函數的實現(Redux1.0.1版本):

export default function compose(...funcs) {
     return funcs.reduceRight((composed, f) => f(composed));
}

  首先先明確一下reduceRight(我用過的次數區區可數,因此介紹一下reducereduceRight)
  

Array.prototype.reduce.reduce(callback, [initialValue])

reduce方法有兩個參數,第一個參數是一個callback,用於針對數組項的操做;第二個參數則是傳入的初始值,這個初始值用於單個數組項的操做。須要注意的是,reduce方法返回值並非數組,而是形如初始值的通過疊加處理後的操做。
callback分別有四個參數:

  1. accumulator:上一次callback返回的累積值

  2. currentValue: 當前值

  3. currentIndex: 當前值索引

  4. array: 數組
    例如:

var sum = [0, 1, 2, 3].reduce(function(a, b) {
return a + b;
}, 0);
// sum is 6

  reducereduceRight的區別就是從左到右和從右到左的區別。因此若是咱們調用compose([func1,func2],store.dispatch)的話,實際返回的函數是:

//也就是當前dispatch的值
func1(func2(store.dispatch))

  勝利在望,看最後一句:

return {
    ...store,
    dispatch
};

  這裏實際上是ES7的用法,至關於ES6中的:

return Object.assign({},store,{dispatch:dispatch});

  或者是Underscore.js中的:

return _.extends({}, store, { dispatch: dispatch });

  懂了吧,就是新建立的一個對象,將store中的全部可枚舉屬性複製進去(淺複製),並用當前的dispatch覆蓋store中的dispatch屬性。因此

let store = applyMiddleware(loggerMiddleware)(createStore)(rootReducer);

中的store中的dispatch屬性已經不是以前的Redux原生的dispatch而是相似於func1(func2(store.dispatch))這種形式的函數了,可是咱們不由又要問了,那麼中間件Midddleware又是怎麼作的呢,咱們看一下以前咱們提供的建議的打印日誌的函數:

export default function createLogger({ getState }) {
      return (next) => 
        (action) => {
              const console = window.console;
              const prevState = getState();
              const returnValue = next(action);
              const nextState = getState();
              const actionType = String(action.type);
              const message = `action ${actionType}`;

              console.log(`%c prev state`, `color: #9E9E9E`, prevState);
              console.log(`%c action`, `color: #03A9F4`, action);
              console.log(`%c next state`, `color: #4CAF50`, nextState);
              return returnValue;
    };
}

  假設一下,咱們如今使用兩個中間件,createLoggercreateMiddleware,其中createMiddleware的函數爲

export default function createMiddleware({ getState }) {
      return (next) => 
        (action) => {
        return next(action)
    };
}

調用形式爲:

let store = applyMiddleware(createLogger,createMiddleware)(createStore)(rootReducer);

若是調用了store.dispatch(action),chain中的兩個函數分別是
createLoggercreateMiddleware中的

(next) => (action) => {}

部分。咱們姑且命名一下chain中關於createLogger的函數叫作
func1,關於createMiddleware的函數叫作func2。那麼如今調用
store.dispatch(action),實際就調用了(注意順序)

//這裏的store.dispatch是原始Redux提供的dispatch函數
func1(func2(store.dispatch))(action)

  上面的函數你們注意以前執行次序,首先func2(store.dispatch再是func1(args)(action)。對於func1得到的next的實參是參數是:

(action)=>{
    //func2中的next是store.dispatch
    next(action);
}

  那麼實際上func1(...)(action)執行的時候,也就是

const console = window.console;
const prevState = getState();
const returnValue = next(action);
const nextState = getState();
const actionType = String(action.type);
const message = `action ${actionType}`;

console.log(`%c prev state`, `color: #9E9E9E`, prevState);
console.log(`%c action`, `color: #03A9F4`, action);
console.log(`%c next state`, `color: #4CAF50`, nextState);
return returnValue;

的時候,getState調用的閉包MiddlewareAPI中的Redux的getState函數,調用next(action)的時候,會回調createMiddleware函數,而後createMiddlewarenext函數會回調真正的store.dispatch(action)。所以咱們能夠看出來實際的調用順序是和傳入中間件順序相反的,例如:

let store = applyMiddleware(Middleware1,Middleware2,Middleware3)(createStore)(rootReducer);

實際的執行是次序是store.dispatch->Middleware3->Middleware2->Middleware1
  不知道你們有沒有注意到一點,

var middlewareAPI = {
    getState: store.getState,
    dispatch: (action) => dispatch(action)
};

並無直接使用dispatch:dispatch,而是使用了dispatch:(action) => dispatch(action),其目的是若是使用了dispatch:dispatch,那麼在全部的Middleware中實際都引用的同一個dispatch(閉包),若是存在一箇中間件修改了dispatch,就會致使後面一下一系列的問題,可是若是使用dispatch:(action) => dispatch(action)就能夠避免這個問題。
  接下來咱們看看異步的action如何實現,咱們先演示一個異步action creater函數:

export const FETCHING_DATA = 'FETCHING_DATA'; // 拉取狀態
export const RECEIVE_USER_DATA = 'RECEIVE_USER_DATA'; //接收到拉取的狀態
export function fetchingData(flag) {
    return {
        type: FETCHING_DATA,
        isFetchingData: flag
    };
}

export function receiveUserData(json) {
    return {
        type: RECEIVE_USER_DATA,
        profile: json
    }
}
export function fetchUserInfo(username) {
    return function (dispatch) {
        dispatch(fetchingData(true));
        return fetch(`https://api.github.com/users/${username}`)
            .then(response => {
                console.log(response);
                return response.json();
            })
            .then(json => {
                console.log(json);
                return json;
            })
            .then((json) => {
                dispatch(receiveUserData(json))
            })
            .then(() => dispatch(fetchingData(false)));
    };
}

  上面的代碼用來從Github API中拉取名爲username的用戶信息,可見首先fetchUserInfo函數會dispatch一個表示開始拉取的action,而後使用fetch函數訪問Github的API,並返回一個Promise,等到獲取到數據的時候,dispatch一個收到數據的action,最後dispatch一個拉取結束的action。由於普通的action都是一個純JavaScript Object對象,可是異步的Action卻返回的是一個function,這是咱們就要使用的一箇中間件:redux-thunk。
  咱們給出一個相似redux-thunk的實現:

export default function thunkMiddleware({ dispatch, getState }) {
      return next => 
             action => 
                   typeof action === ‘function’ ? 
                     action(dispatch, getState) : 
                     next(action);
}

這個和你以前看到的中間件很相似。若是獲得的action是個函數,就用dispatch和getState看成參數來調用它,不然就直接分派給store。從而實現異步的Action。
  Redux入門學習,若是有寫的不對的地方,但願你們指正,歡迎你們圍觀個人博客:
  
  MrErHu
  SegmentFault

相關文章
相關標籤/搜索