精益 React 學習指南 (Lean React)- 3.4 掌控 redux 異步

書籍完整目錄javascript

3.4 redux 異步

圖片描述

在大多數的前端業務場景中,須要和後端產生異步交互,在本節中,將詳細講解 redux 中的異步方案以及一些異步第三方組件,內容有:前端

  • redux 異步流java

  • redux-thunkgit

  • redux-promisegithub

  • redux-sagaajax

3.4.1 redux 異步流

前面講的 redux 中的數據流都是同步的,流程以下:shell

view -> actionCreator -> action -> reducer -> newState -> container component
但同步數據不能知足真實業務開發,真實業務中異步纔是主角,那如何將異步處理結合到上邊的流程中呢?express

3.4.2 實現異步的方式

其實 redux 並未有和異步相關的概念,咱們能夠用任何原來實現異步的方式應用到 redux 數據流中,最簡單的方式就是延遲 dispatch action,以 setTimeout 爲例:npm

this.dispatch({ type: 'SYNC_SOME_ACTION'})
window.setTimeout(() => {
  this.dispatch({ type: 'ASYNC_SOME_ACTION' })
}, 1000)

這種方式最簡單直接,可是有以下問題:redux

  1. 若是有多個相似的 action 觸發場景,異步邏輯不能重用

  2. 異步處理代碼不能統一處理,最簡單的例子就是節流

解決上面兩個問題的辦法很簡單,把異步的代碼剝離出來:

someAction.js

function dispatchSomeAction(dispatch, payload) {
    // ..調用控制邏輯...
    dispatch({ type: 'SYNC_SOME_ACTION'})
    window.setTimeout(() => {
      dispatch({ type: 'ASYNC_SOME_ACTION' })
    }, 1000)
}

而後組件只須要調用:

import {dispatchSomeAction} from 'someAction.js'

dispatchSomeAction(dispatch, payload);

基於這種方式上面的流程就改成了:

view -> asyncActionDispatcher -> wait -> action -> reducer -> newState -> container component

asyncActionDispatcher 和 actionCreator 是十分相似的, 因此簡單而言就能夠把它理解爲 asyncActionCreator , 因此新的流程爲:

view -> asyncActionCreator -> wait -> action -> reducer -> newState -> container component

可是上面的方法有一些缺點

同步調用和異步調用的方式不相同:

  • 同步的狀況: store.dispatch(actionCreator(payload))

  • 異步的狀況: asyncActionCreator(store.dispatch, payload)

幸運的是在 redux 中經過 middleware 機制能夠很容易的解決上面的問題

經過 middleware 實現異步

咱們已經很清楚一個 middleware 的結構 ,其核心的部分爲

function(action) {
    // 調用後面的 middleware
    next(action)
}

middleware 徹底掌控了 reducer 的觸發時機, 也就是 action 到了這裏徹底由中間件控制,不樂意就不給其餘中間件處理的機會,並且還能夠控制調用其餘中間件的時機。

舉例來講一個異步的 ajax 請求場景,能夠以下實現:

function (action) {
    // async call 
    fetch('....')
      .then(
          function resolver(ret) {
            newAction = createNewAction(ret, action)
            next(newAction)
          },
          function rejector(err) {
            rejectAction = createRejectAction(err, action)
            next(rejectAction)
          })
    });
}

任何異步的 javascript 邏輯均可以,如: ajax callback, Promise, setTimeout 等等, 也可使用 es7 的 async 和 await。

第三方異步組件

上面的實現方案只是針對具體的場景設計的,那若是是如何解決通用場景下的問題呢,其實目前已經有不少第三方 redux 組件支持異步 action,其中如:

這些組件都有很好的擴展性,徹底能知足咱們開發異步流程的場景,下面來一一介紹

3.4.3 redux-thunk

redux-thunk 介紹

redux-thunk 是 redux 官方文檔中用到的異步組件,實質就是一個 redux 中間件,thunk 聽起來是一個很陌生的詞語,先來認識一下什麼叫 thunk

A thunk is a function that wraps an expression to delay its evaluation.

簡單來講一個 thunk 就是一個封裝表達式的函數,封裝的目的是延遲執行表達式

// 1 + 2 當即被計算 = 3
let x = 1 + 2;

// 1 + 2 被封裝在了 foo 函數內
// foo 能夠被延遲執行
// foo 就是一個 thunk 
let foo = () => 1 + 2;

redux-thunk 是一個通用的解決方案,其核心思想是讓 action 能夠變爲一個 thunk ,這樣的話:

  1. 同步狀況:dispatch(action)

  2. 異步狀況:dispatch(thunk)

咱們已經知道了 thunk 本質上就是一個函數,函數的參數爲 dispatch, 因此一個簡單的 thunk 異步代碼就是以下:

this.dispatch(function (dispatch){
    setTimeout(() => {
       dispatch({type: 'THUNK_ACTION'}) 
    }, 1000)
})

以前已經講過,這樣的設計會致使異步邏輯放在了組件中,解決辦法爲抽象出一個 asyncActionCreator, 這裏也同樣,咱們就叫 thunkActionCreator 吧,上面的例子能夠改成:

//actions/someThunkAction.js
export function createThunkAction(payload) {
    return function(dispatch) {
        setTimeout(() => {
           dispatch({type: 'THUNK_ACTION', payload: payload}) 
        }, 1000)
    }
}

// someComponent.js
this.dispatch(createThunkAction(payload))

安裝和使用

第一步:安裝

$ npm install redux-thunk

第二步: 添加 thunk 中間件

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

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

第三步:實現一個 thunkActionCreator

//actions/someThunkAction.js
export function createThunkAction(payload) {
    return function(dispatch) {
        setTimeout(() => {
           dispatch({type: 'THUNK_ACTION', payload: payload}) 
        }, 1000)
    }
}

第三步:組件中 dispatch thunk

this.dispatch(createThunkAction(payload));

擁有 dispatch 方法的組件爲 redux 中的 container component

thunk 源碼

說了這麼多,redux-thunk 是否是作了不少工做,實現起來很複雜,那咱們來看看 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;

就這麼簡單,只有 14 行源碼,可是這簡短的實現卻能完成複雜的異步處理,怎麼作到的,咱們來分析一下:

  1. 判斷若是 action 是 function 那麼執行 action(dispatch, getState, ...)

    1. action 也就是一個 thunk

    2. 執行 action 至關於執行了異步邏輯

      1. action 中執行 dispatch

      2. 開始新的 redux 數據流,從新回到最開始的邏輯(thunk 能夠嵌套的緣由)

    3. 把執行的結果做爲返回值直接返回

    4. 直接返回並無調用其餘中間件,也就意味着中間件的執行在這裏中止了

    5. 能夠對返回值作處理(後面會講若是返回值是 Promise 的狀況)

  2. 若是不是函數直接調用其餘中間件並返回

理解了這個事後是否是對 redux-thunk 的使用思路變得清晰了

thunk 的組合

根據 redux-thunk 的特性,能夠作出頗有意思的事情

  1. 能夠遞歸的 dispatch(thunk) => 實現 thunk 的組合;

  2. thunk 運行結果會做爲 dispatch返回值 => 利用返回值爲 Promise 能夠實現多個 thunk 的編排;

thunk 組合例子:

function thunkC() {
    return function(dispatch) {
        dispatch(thunkB())
    }
}
function thunkB() {
    return function (dispatch) {
        dispatch(thunkA())
    }
}
function thunkA() {
    return function (dispatch) {
        dispatch({type: 'THUNK_ACTION'})
    }
}

Promise 例子

function ajaxCall() {
    return fetch(...);
}

function thunkC() {
    return function(dispatch) {
        dispatch(thunkB(...))
        .then(
            data => dispatch(thunkA(data)),
            err  => dispatch(thunkA(err))
        )
    }
}
function thunkB() {
    return function (dispatch) {
        return ajaxCall(...)
    }
}

function thunkA() {
    return function (dispatch) {
        dispatch({type: 'THUNK_ACTION'})
    }
}

3.4.4 redux-promise

另一個 redux 文檔中提到的異步組件爲 redux-promise, 咱們直接分析一下其源碼吧

import { isFSA } from 'flux-standard-action';

function isPromise(val) {
  return val && typeof val.then === 'function';
}

export default function promiseMiddleware({ dispatch }) {
  return next => action => {
    if (!isFSA(action)) {
      return isPromise(action)
        ? action.then(dispatch)
        : next(action);
    }

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

大概的邏輯就是:

  1. 若是不是標準的 flux action,那麼判斷是不是 promise, 是執行 action.then(dispatch),否執行 next(action)

  2. 若是是標準的 flux action, 判斷 payload 是不是 promise,是的話 payload.then 獲取數據,而後把數據做爲 payload 從新 dispatch({ ...action, payload: result}) , 否執行 next(action)

結合 redux-promise 能夠利用 es7 的 async 和 await 語法,簡化異步的 promiseActionCreator 的設計, eg:

export default async (payload) => {
  const result = await somePromise;
  return {
    type: "PROMISE_ACTION",
    payload: result.someValue;
  }
}

若是對 es7 async 語法不是很熟悉能夠看下面兩個例子:

  1. async 關鍵字能夠老是返回一個 Promise 的 resolve 結果或者 reject 結果

async function foo() {
    if(true)
        return 'Success!';
    else
        throw 'Failure!';
}

// 等價於
 
function foo() {
    if(true)
        return Promise.resolve('Success!');
    else
        return Promise.reject('Failure!');
}
  1. 在 async 關鍵字中可使用 await 關鍵字,其目的是 await 一個 promise, 等待 promise resolve 和 reject

eg:

async function foo(aPromise) {
    const a = await new Promise(function(resolve, reject) {
            // This is only an example to create asynchronism
            window.setTimeout(
                function() {
                    resolve({a: 12});
                }, 1000);
        })
    console.log(a.a)
    return  a.a
}

// in console
> foo() 
> Promise {_c: Array[0], _a: undefined, _s: 0, _d: false, _v: undefined…}
> 12

能夠看到在控制檯中,先返回了一個 promise,而後輸出了 12

async 關鍵字能夠極大的簡化異步流程的設計,避免 callback 和 thennable 的調用,看起來和同步代碼一致。

3.4.5 redux-saga

redux-saga 介紹

redux-saga 也是解決 redux 異步 action 的一箇中間件,不過和以前的設計有本質的不一樣

  1. redux-saga 徹底基於 Es6 的 Generator Function

  2. 不使用 actionCreator 策略,而是經過監控 action, 而後在自動作處理

  3. 全部帶反作用的操做(異步代碼,不肯定的代碼)都被放到 saga 中

那到底什麼是 saga

redux-saga 實際也沒有解釋什麼叫 saga ,經過引用的參考:

The term saga is commonly used in discussions of CQRS to refer to a piece of code that coordinates and routes messages between bounded contexts and aggregates.

這個定義的核心就是 CQRS-查詢與責任分離 ,對應到 redux-sage 就是 action 與 處理函數的分離。 實際上在 redux-saga 中,一個 saga 就是一個 Generator 函數。

eg:

import { takeEvery, takeLatest } from 'redux-saga'
import { call, put } from 'redux-saga/effects'
import Api from '...'

/*
 * 一個 saga 就是一個 Generator Function 
 *
 * 每當 store.dispatch `USER_FETCH_REQUESTED` action 的時候都會調用 fetchUser.
 */
function* mySaga() {
  yield* takeEvery("USER_FETCH_REQUESTED", fetchUser);
}

/**
 * worker saga: 真正處理 action 的 saga
 *  
 * USER_FETCH_REQUESTED action 觸發時被調用
 * @param {[type]} action  [description]
 * @yield {[type]} [description]
 */
function* fetchUser(action) {
   try {
      const user = yield call(Api.fetchUser, action.payload.userId);
      yield put({type: "USER_FETCH_SUCCEEDED", user: user});
   } catch (e) {
      yield put({type: "USER_FETCH_FAILED", message: e.message});
   }
}

一些基本概念

watcher saga

負責編排和派發任務的 saga

worker saga

真正負責處理 action 的函數

saga helper

如上面例子中的 takeEvery,簡單理解就是用於監控 action 並派發 action 到 worker saga 的輔助函數

Effect

redux-saga 徹底基於 Generator 構建,saga 邏輯的表達是經過 yield javascript 對象來實現,這些對象就是Effects。

這些對象至關於描述任務的規範化數據(任務如執行異步函數,dispatch action 到一個 store),這些數據被髮送到 redux-saga 中間件中執行,如:

  1. put({type: "USER_FETCH_SUCCEEDED", user: user}) 表示要執行 dispatch({{type: "USER_FETCH_SUCCEEDED", user: user}}) 任務

  2. call(fetch, url) 表示要執行 fetch(url)

經過這種 effect 的抽象,能夠避免 call 和 dispatch 的當即執行,而是描述要執行什麼任務,這樣的話就很容易對 saga 進行測試,saga 所作的事情就是將這些 effect 編排起來用於描述任務,真正的執行都會放在 middleware 中執行。

安裝和使用

第一步:安裝

$ npm install --save redux-saga

第二步:添加 saga 中間件

import { createStore, applyMiddleware } from 'redux'
import createSagaMiddleware from 'redux-saga'

import reducer from './reducers'
import mySaga from './sagas'

// 建立 saga 中間件
const sagaMiddleware = createSagaMiddleware()

// 添加到中間件中
const store = createStore(
  reducer,
  applyMiddleware(sagaMiddleware)
)

// 當即運行 saga ,讓監控器開始監控
sagaMiddleware.run(mySaga)

第三步:定義 sagas/index.js

import { takeEvery } from 'redux-saga'
import { put } from 'redux-saga/effects'

export const delay = ms => new Promise(resolve => setTimeout(resolve, ms))

// 將異步執行 increment 任務
export function* incrementAsync() {
  yield delay(1000)
  yield put({ type: 'INCREMENT' })
}

// 在每一個 INCREMENT_ASYNC action 調用後,派生一個新的 incrementAsync 任務
export default function* watchIncrementAsync() {
  yield* takeEvery('INCREMENT_ASYNC', incrementAsync)
}

第四步:組件中調用

this.dispatch({type: 'INCREMENT_ASYNC'})

redux-saga 基於 Generator 有不少高級的特性, 如:

  1. 基於 take Effect 實現更自由的任務編排

  2. fork 和 cancel 實現非阻塞任務

  3. 並行任何和 race 任務

  4. saga 組合 ,yield* saga

因篇幅有限,這部份內容在下一篇講解

相關文章
相關標籤/搜索