博客 / 詳情

返回

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

書籍完整目錄

3.4 redux 異步

圖片描述

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

  • redux 異步流

  • redux-thunk

  • redux-promise

  • redux-saga

3.4.1 redux 異步流

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

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

3.4.2 實現異步的方式

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

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

這種方式最簡單直接,但是有如下問題:

  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,其中如:

  • redux-thunk

  • redux-promise

  • redux-saga

這些組件都有很好的擴展性,完全能滿足我們開發異步流程的場景,下面來一一介紹

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

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

user avatar tingzhong666 頭像 niumingxin 頭像
2 位用戶收藏了這個故事!

發佈 評論

Some HTML is okay.