Redux初探與異步數據流

基本認知

先貼一張redux的基本結構圖
redux
原圖來自《UNIDIRECTIONAL USER INTERFACE ARCHITECTURES》html

在這張圖中,咱們能夠很清晰的看到,view中產生action,經過store.dispatch(action)將action交由reducer處理,最終根據處理的結果更新view。
在這個過程當中,action是簡單對象,用於描述一個動做以及對應於該動做的數據。例如:前端

const ADD_TODO = 'ADD_TODO';

// action
{
    type: ADD_TODO,
    data: 'some data'
}

而reducer則是純函數,且是冪等的,即只要傳入參數相同,返回計算獲得的下一個 state 就必定相同。沒有特殊狀況、沒有反作用,沒有 API 請求、沒有變量修改,單純執行計算。react

同步數據流

在擁有了以上基本認知以後,咱們來看下redux究竟是如何工做的。Talk is cheap, show me the code.express

import React from 'react'  
import { createStore, bindActionCreators } from 'redux'
import { connect } from 'react-redux' 
import ReactDom from 'react-dom'
import { Provider } from 'react-redux'

function createAction() {
  return {
    type: 'ADD_TODO',
    data: 'some data'
  }
}  

class App extends React.Component {
    constructor() {
        super();
    }
    render() {
        return (
            <div style={{width:'200px', height:'200px',margin:'100px',border:'2px solid black'}}>
                <div onClick={this.props.actions.createAction.bind(this)}>
                  {"Click Me!"}
                </div>
            </div>
        );
    }
}

function mapStateToProps(state) {
  return {
    data: state
  }
}

function mapDispatchToProps(dispatch) {
  return {
    actions: bindActionCreators({createAction}, dispatch)
  }
}

var AppApp = connect(
  mapStateToProps,
  mapDispatchToProps
)(App);

function reducer(state, action) {
  console.log(action);
  return state;
}

var store = createStore(reducer);
ReactDom.render(
  <Provider store={store}>
    <AppApp />
  </Provider>,
  document.getElementById('container')
);

這是一個精簡版本的redux demo,每點擊一次「Click Me!」,控制檯會打印一次action。json

因爲篇幅限制,以上代碼未分模塊redux

下面是截圖:
效果圖數組

控制檯打印輸出:
控制檯打印promise

從上面代碼中能夠清晰的看出,當用戶點擊「Click Me!」的時候,會當即調用createAction產生一個action,以後redux獲取這個action並調用store.dispatch將這個action丟給reducer進行處理,demo中的reducer僅僅打印了action。
數據從view中流出,經reducer處理後又回到了view。
至此,咱們看到的一切都是跟上面的基本認知是一致的。app

接下來講說異步數據流,這塊也是困擾了我很久,直到最近才搞清楚內在緣由。dom

Redux Middleware

redux爲咱們作了不少的事情,咱們均可以不用經過顯示的調用dispatch函數就將咱們的action傳遞給reducer。這在前面的demo中就能夠看到。可是至此,redux一直沒有解決異步的問題。試想,若是我在頁面輸入一段內容,而後觸發了一個搜索動做,此時須要向服務端請求數據並將返回的數據展現出來。這是一個很常見的功能,可是涉及到異步請求,剛剛的demo中的方法已經再也不適用了。那麼redux是如何解決異步問題的呢?

沒錯,就是引入middleware。middleware,顧名思義就是中間件。用過express的同窗對中間件應該都很熟悉。其實在redux中,middleware並不只僅用於解決異步的問題,它還能夠作不少其餘的事情,好比記錄日誌、錯誤報告、路由等等。

關於redux middleware的說明在官方文檔中已經有了很是清晰的說明,中文版英文版都有,這裏就不在贅述,只摘錄一句話,說明以下。

It provides a third-party extension point between dispatching an action, and the moment it reaches the reducer.

這裏我想說下redux middleware的具體實現,我也正是從源代碼中找到了困擾個人問題的緣由。
先看applyMiddleware(...middlewares)的代碼:

import compose from './compose'
export default function applyMiddleware(...middlewares) {
  return (createStore) => (reducer, initialState, enhancer) => {
    var store = createStore(reducer, initialState, enhancer)
    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
    }
  }
}

代碼很短,此處咱們只關注最內層函數的實現。在建立了store之後,咱們對傳進來的每個middleware進行以下處理:

var middlewareAPI = {
      getState: store.getState,
      dispatch: (action) => dispatch(action)
    }
    chain = middlewares.map(middleware => middleware(middlewareAPI))

處理後獲得一個數組保存在chain中。以後將chain傳給compose,並將store.dispatch傳給返回的函數。那麼在這裏面作了什麼呢?咱們再看compose的實現:

export default function compose(...funcs) {
  if (funcs.length === 0) {
    return arg => arg
  } else {
    const last = funcs[funcs.length - 1]
    const rest = funcs.slice(0, -1)
    return (...args) => rest.reduceRight((composed, f) => f(composed), last(...args))
  }
}

compose中的核心動做就是將傳進來的全部函數倒序(reduceRight)進行以下處理:

(composed, f) => f(composed)

咱們知道Array.prototype.reduceRight是從右向左累計計算的,會將上一次的計算結果做爲本次計算的輸入。再看看applyMiddleware中的調用代碼:

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

compose函數最終返回的函數被做爲了dispatch函數,結合官方文檔和代碼,不可貴出,中間件的定義形式爲:

function middleware({dispatch, getState}) {
    return function (next) {
        return function (action) {
            return next(action);
        }
    }
}

或  

middleware = (dispatch, getState) => next => action => {
    next(action);
}

也就是說,redux的中間件是一個函數,該函數接收dispatch和getState做爲參數,返回一個以dispatch爲參數的函數,這個函數的返回值是接收action爲參數的函數(能夠看作另外一個dispatch函數)。在中間件鏈中,以dispatch爲參數的函數的返回值將做爲下一個中間件(準確的說應該是返回值)的參數,下一個中間件將它的返回值接着往下一個中間件傳遞,最終實現了store.dispatch在中間件間的傳遞。

看了中間件的文檔和代碼以後,我算是搞明白了中間件的原理。以前一直困擾個人問題如今看來實際上是概念問題(此處不提也罷),中間件只關注dispatch函數的傳遞,至於在傳遞的過程當中幹了什麼中間件並不關心。

下面看看經過中間件,咱們如何實現異步調用。這裏就不得不提redux-thunk中間件了。

redux-thunk

redux與redux-thunk是同一個做者。

咱們知道,異步調用何時返回前端是沒法控制的。對於redux這條嚴密的數據流來講,如何才能作到異步呢。redux-thunk的基本思想就是經過函數來封裝異步請求,也就是說在actionCreater中返回一個函數,在這個函數中進行異步調用。咱們已經知道,redux中間件只關注dispatch函數的傳遞,並且redux也不關心dispatch函數的返回值,因此只須要讓redux認識這個函數就能夠了。
看了一下redux-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;

這段代碼跟上面咱們看到的中間件沒有太大的差異,惟一一點就是對action作了一下以下判斷:

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

也就是說,若是發現actionCreater傳過來的action是一個函數的話,會執行一下這個函數,並以這個函數的返回值做爲返回值。前面已經說過,redux對dispatch函數的返回值不是很關心,所以此處也就無所謂了。

這樣的話,在咱們的actionCreater中,咱們就能夠作任何的異步調用了,而且返回任何值也無所謂,因此咱們可使用promise了:

function actionCreate() {
    return function (dispatch, getState) {
        // 返回的函數體內自由實現。。。
        Ajax.fetch({xxx}).then(function (json) {
            dispatch(json);
        })
    }
}

經過redux-thunk,咱們將異步的操做融合進了現有的數據流中。
最後還須要注意一點,因爲中間件只關心dispatch的傳遞,並不限制你作其餘的事情,所以咱們最好將redux-thunk放到中間件列表的首位,防止其餘中間件中返回異步請求。

小結

以上是最近一段時間學習和思考的總結。在這期間發現,學習新知識的基礎是要把概念理解清楚,不能一味的看樣例跑demo,不理解概念對demo也只是知其然不知其因此然,很容易陷入一些經過樣例代碼理解出來的錯誤的概念中,後面再糾正就須要花費不少時間和精力了!

相關文章
相關標籤/搜索