redux 異步流javascript
redux-thunk前端
redux-promisejava
redux-sagagit
前面講的 redux 中的數據流都是同步的,流程以下:github
view -> actionCreator -> action -> reducer -> newState -> container component
但同步數據不能知足真實業務開發,真實業務中異步纔是主角,那如何將異步處理結合到上邊的流程中呢?ajax
其實 redux 並未有和異步相關的概念,咱們能夠用任何原來實現異步的方式應用到 redux 數據流中,最簡單的方式就是延遲 dispatch action,以 setTimeout 爲例:sql
this.dispatch({ type: 'SYNC_SOME_ACTION'})
window.setTimeout(() => {
this.dispatch({ type: 'ASYNC_SOME_ACTION' })
}, 1000)
這種方式最簡單直接,可是有以下問題:shell
若是有多個相似的 action 觸發場景,異步邏輯不能重用express
異步處理代碼不能統一處理,最簡單的例子就是節流npm
解決上面兩個問題的辦法很簡單,把異步的代碼剝離出來:
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 的結構 ,其核心的部分爲
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 官方文檔中用到的異步組件,實質就是一個 redux 中間件,thunk 聽起來是一個很陌生的詞語,先來認識一下什麼叫 thunk
A thunk is a function that wraps an expression to delay its evaluation.
簡單來講一個 thunk 就是一個封裝表達式的函數,封裝的目的是延遲執行表達式
// 1 + 2 當即被計算 = 3let x = 1 + 2;
// 1 + 2 被封裝在了 foo 函數內// foo 能夠被延遲執行// foo 就是一個 thunk let foo = () => 1 + 2;
redux-thunk 是一個通用的解決方案,其核心思想是讓 action 能夠變爲一個 thunk ,這樣的話:
同步狀況:dispatch(action)
異步狀況: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.jsthis.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
說了這麼多,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 行源碼,可是這簡短的實現卻能完成複雜的異步處理,怎麼作到的,咱們來分析一下:
判斷若是 action 是 function 那麼執行 action(dispatch, getState, ...)
action 也就是一個 thunk
執行 action 至關於執行了異步邏輯
action 中執行 dispatch
開始新的 redux 數據流,從新回到最開始的邏輯(thunk 能夠嵌套的緣由)
把執行的結果做爲返回值直接返回
直接返回並無調用其餘中間件,也就意味着中間件的執行在這裏中止了
能夠對返回值作處理(後面會講若是返回值是 Promise 的狀況)
若是不是函數直接調用其餘中間件並返回
理解了這個事後是否是對 redux-thunk 的使用思路變得清晰了
根據 redux-thunk 的特性,能夠作出頗有意思的事情
能夠遞歸的 dispatch(thunk) => 實現 thunk 的組合;
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'})
}
}
另一個 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);
};
}
大概的邏輯就是:
若是不是標準的 flux action,那麼判斷是不是 promise, 是執行 action.then(dispatch),否執行 next(action)
若是是標準的 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 語法不是很熟悉能夠看下面兩個例子:
async 關鍵字能夠老是返回一個 Promise 的 resolve 結果或者 reject 結果
async function foo() {
if(true)
return 'Success!';
elsethrow 'Failure!';
}
// 等價於function foo() {
if(true)
return Promise.resolve('Success!');
elsereturn Promise.reject('Failure!');
}
在 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 asynchronismwindow.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 的調用,看起來和同步代碼一致。
redux-saga 也是解決 redux 異步 action 的一箇中間件,不過和以前的設計有本質的不一樣
redux-saga 徹底基於 Es6 的 Generator Function
不使用 actionCreator 策略,而是經過監控 action, 而後在自動作處理
全部帶反作用的操做(異步代碼,不肯定的代碼)都被放到 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 中間件中執行,如:
put({type: "USER_FETCH_SUCCEEDED", user: user})
表示要執行 dispatch({{type: "USER_FETCH_SUCCEEDED", user: user}})
任務
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 有不少高級的特性, 如:
基於 take Effect 實現更自由的任務編排
fork 和 cancel 實現非阻塞任務
並行任何和 race 任務
saga 組合 ,yield* saga