精益 React 学习指南 (Lean React)- 3.4 掌控 redux 异步
書籍完整目錄
3.4 redux 異步
在大多數(shù)的前端業(yè)務(wù)場(chǎng)景中,需要和后端產(chǎn)生異步交互,在本節(jié)中,將詳細(xì)講解 redux 中的異步方案以及一些異步第三方組件,內(nèi)容有:
redux 異步流
redux-thunk
redux-promise
redux-saga
3.4.1 redux 異步流
前面講的 redux 中的數(shù)據(jù)流都是同步的,流程如下:
view -> actionCreator -> action -> reducer -> newState -> container component
但同步數(shù)據(jù)不能滿足真實(shí)業(yè)務(wù)開發(fā),真實(shí)業(yè)務(wù)中異步才是主角,那如何將異步處理結(jié)合到上邊的流程中呢?
3.4.2 實(shí)現(xiàn)異步的方式
其實(shí) redux 并未有和異步相關(guān)的概念,我們可以用任何原來實(shí)現(xiàn)異步的方式應(yīng)用到 redux 數(shù)據(jù)流中,最簡單的方式就是延遲 dispatch action,以 setTimeout 為例:
this.dispatch({ type: 'SYNC_SOME_ACTION'}) window.setTimeout(() => {this.dispatch({ type: 'ASYNC_SOME_ACTION' }) }, 1000)這種方式最簡單直接,但是有如下問題:
如果有多個(gè)類似的 action 觸發(fā)場(chǎng)景,異步邏輯不能重用
異步處理代碼不能統(tǒng)一處理,最簡單的例子就是節(jié)流
解決上面兩個(gè)問題的辦法很簡單,把異步的代碼剝離出來:
someAction.js
function dispatchSomeAction(dispatch, payload) {// ..調(diào)用控制邏輯...dispatch({ type: 'SYNC_SOME_ACTION'})window.setTimeout(() => {dispatch({ type: 'ASYNC_SOME_ACTION' })}, 1000) }然后組件只需要調(diào)用:
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
但是上面的方法有一些缺點(diǎn)
同步調(diào)用和異步調(diào)用的方式不相同:
同步的情況: store.dispatch(actionCreator(payload))
異步的情況: asyncActionCreator(store.dispatch, payload)
幸運(yùn)的是在 redux 中通過 middleware 機(jī)制可以很容易的解決上面的問題
通過 middleware 實(shí)現(xiàn)異步
我們已經(jīng)很清楚一個(gè) middleware 的結(jié)構(gòu) ,其核心的部分為
function(action) {// 調(diào)用后面的 middlewarenext(action) }middleware 完全掌控了 reducer 的觸發(fā)時(shí)機(jī), 也就是 action 到了這里完全由中間件控制,不樂意就不給其他中間件處理的機(jī)會(huì),而且還可以控制調(diào)用其他中間件的時(shí)機(jī)。
舉例來說一個(gè)異步的 ajax 請(qǐng)求場(chǎng)景,可以如下實(shí)現(xiàn):
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。
第三方異步組件
上面的實(shí)現(xiàn)方案只是針對(duì)具體的場(chǎng)景設(shè)計(jì)的,那如果是如何解決通用場(chǎng)景下的問題呢,其實(shí)目前已經(jīng)有很多第三方 redux 組件支持異步 action,其中如:
redux-thunk
redux-promise
redux-saga
這些組件都有很好的擴(kuò)展性,完全能滿足我們開發(fā)異步流程的場(chǎng)景,下面來一一介紹
3.4.3 redux-thunk
redux-thunk 介紹
redux-thunk 是 redux 官方文檔中用到的異步組件,實(shí)質(zhì)就是一個(gè) redux 中間件,thunk 聽起來是一個(gè)很陌生的詞語,先來認(rèn)識(shí)一下什么叫 thunk
A thunk is a function that wraps an expression to delay its evaluation.
簡單來說一個(gè) thunk 就是一個(gè)封裝表達(dá)式的函數(shù),封裝的目的是延遲執(zhí)行表達(dá)式
// 1 + 2 立即被計(jì)算 = 3 let x = 1 + 2;// 1 + 2 被封裝在了 foo 函數(shù)內(nèi) // foo 可以被延遲執(zhí)行 // foo 就是一個(gè) thunk let foo = () => 1 + 2;redux-thunk 是一個(gè)通用的解決方案,其核心思想是讓 action 可以變?yōu)橐粋€(gè) thunk ,這樣的話:
同步情況:dispatch(action)
異步情況:dispatch(thunk)
我們已經(jīng)知道了 thunk 本質(zhì)上就是一個(gè)函數(shù),函數(shù)的參數(shù)為 dispatch, 所以一個(gè)簡單的 thunk 異步代碼就是如下:
this.dispatch(function (dispatch){setTimeout(() => {dispatch({type: 'THUNK_ACTION'}) }, 1000) })之前已經(jīng)講過,這樣的設(shè)計(jì)會(huì)導(dǎo)致異步邏輯放在了組件中,解決辦法為抽象出一個(gè) 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) );第三步:實(shí)現(xiàn)一個(gè) 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 是不是做了很多工作,實(shí)現(xiàn)起來很復(fù)雜,那我們來看看 thunk 中間件的實(shí)現(xiàn)
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 行源碼,但是這簡短的實(shí)現(xiàn)卻能完成復(fù)雜的異步處理,怎么做到的,我們來分析一下:
判斷如果 action 是 function 那么執(zhí)行 action(dispatch, getState, ...)
action 也就是一個(gè) thunk
執(zhí)行 action 相當(dāng)于執(zhí)行了異步邏輯
action 中執(zhí)行 dispatch
開始新的 redux 數(shù)據(jù)流,重新回到最開始的邏輯(thunk 可以嵌套的原因)
把執(zhí)行的結(jié)果作為返回值直接返回
直接返回并沒有調(diào)用其他中間件,也就意味著中間件的執(zhí)行在這里停止了
可以對(duì)返回值做處理(后面會(huì)講如果返回值是 Promise 的情況)
如果不是函數(shù)直接調(diào)用其他中間件并返回
理解了這個(gè)過后是不是對(duì) redux-thunk 的使用思路變得清晰了
thunk 的組合
根據(jù) redux-thunk 的特性,可以做出很有意思的事情
可以遞歸的 dispatch(thunk) => 實(shí)現(xiàn) thunk 的組合;
thunk 運(yùn)行結(jié)果會(huì)作為 dispatch返回值 => 利用返回值為 Promise 可以實(shí)現(xiàn)多個(gè) 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
另外一個(gè) 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);}; }大概的邏輯就是:
如果不是標(biāo)準(zhǔn)的 flux action,那么判斷是否是 promise, 是執(zhí)行 action.then(dispatch),否執(zhí)行 next(action)
如果是標(biāo)準(zhǔn)的 flux action, 判斷 payload 是否是 promise,是的話 payload.then 獲取數(shù)據(jù),然后把數(shù)據(jù)作為 payload 重新 dispatch({ ...action, payload: result}) , 否執(zhí)行 next(action)
結(jié)合 redux-promise 可以利用 es7 的 async 和 await 語法,簡化異步的 promiseActionCreator 的設(shè)計(jì), eg:
export default async (payload) => {const result = await somePromise;return {type: "PROMISE_ACTION",payload: result.someValue;} }如果對(duì) es7 async 語法不是很熟悉可以看下面兩個(gè)例子:
async 關(guān)鍵字可以總是返回一個(gè) Promise 的 resolve 結(jié)果或者 reject 結(jié)果
在 async 關(guān)鍵字中可以使用 await 關(guān)鍵字,其目的是 await 一個(gè) 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可以看到在控制臺(tái)中,先返回了一個(gè) promise,然后輸出了 12
async 關(guān)鍵字可以極大的簡化異步流程的設(shè)計(jì),避免 callback 和 thennable 的調(diào)用,看起來和同步代碼一致。
3.4.5 redux-saga
redux-saga 介紹
redux-saga 也是解決 redux 異步 action 的一個(gè)中間件,不過和之前的設(shè)計(jì)有本質(zhì)的不同
redux-saga 完全基于 Es6 的 Generator Function
不使用 actionCreator 策略,而是通過監(jiān)控 action, 然后在自動(dòng)做處理
所有帶副作用的操作(異步代碼,不確定的代碼)都被放到 saga 中
那到底什么是 saga
redux-saga 實(shí)際也沒有解釋什么叫 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.
這個(gè)定義的核心就是 CQRS-查詢與責(zé)任分離 ,對(duì)應(yīng)到 redux-sage 就是 action 與 處理函數(shù)的分離。 實(shí)際上在 redux-saga 中,一個(gè) saga 就是一個(gè) Generator 函數(shù)。
eg:
import { takeEvery, takeLatest } from 'redux-saga' import { call, put } from 'redux-saga/effects' import Api from '...'/** 一個(gè) saga 就是一個(gè) Generator Function ** 每當(dāng) store.dispatch `USER_FETCH_REQUESTED` action 的時(shí)候都會(huì)調(diào)用 fetchUser.*/ function* mySaga() {yield* takeEvery("USER_FETCH_REQUESTED", fetchUser); }/*** worker saga: 真正處理 action 的 saga* * USER_FETCH_REQUESTED action 觸發(fā)時(shí)被調(diào)用* @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
負(fù)責(zé)編排和派發(fā)任務(wù)的 saga
worker saga
真正負(fù)責(zé)處理 action 的函數(shù)
saga helper
如上面例子中的 takeEvery,簡單理解就是用于監(jiān)控 action 并派發(fā) action 到 worker saga 的輔助函數(shù)
Effect
redux-saga 完全基于 Generator 構(gòu)建,saga 邏輯的表達(dá)是通過 yield javascript 對(duì)象來實(shí)現(xiàn),這些對(duì)象就是Effects。
這些對(duì)象相當(dāng)于描述任務(wù)的規(guī)范化數(shù)據(jù)(任務(wù)如執(zhí)行異步函數(shù),dispatch action 到一個(gè) store),這些數(shù)據(jù)被發(fā)送到 redux-saga 中間件中執(zhí)行,如:
put({type: "USER_FETCH_SUCCEEDED", user: user}) 表示要執(zhí)行 dispatch({{type: "USER_FETCH_SUCCEEDED", user: user}}) 任務(wù)
call(fetch, url) 表示要執(zhí)行 fetch(url)
通過這種 effect 的抽象,可以避免 call 和 dispatch 的立即執(zhí)行,而是描述要執(zhí)行什么任務(wù),這樣的話就很容易對(duì) saga 進(jìn)行測(cè)試,saga 所做的事情就是將這些 effect 編排起來用于描述任務(wù),真正的執(zhí)行都會(huì)放在 middleware 中執(zhí)行。
安裝和使用
第一步:安裝
$ npm install --save redux-saga第二步:添加 saga 中間件
import { createStore, applyMiddleware } from 'redux' import createSagaMiddleware from 'redux-saga'import reducer from './reducers' import mySaga from './sagas'// 創(chuàng)建 saga 中間件 const sagaMiddleware = createSagaMiddleware()// 添加到中間件中 const store = createStore(reducer,applyMiddleware(sagaMiddleware) )// 立即運(yùn)行 saga ,讓監(jiān)控器開始監(jiān)控 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))// 將異步執(zhí)行 increment 任務(wù) export function* incrementAsync() {yield delay(1000)yield put({ type: 'INCREMENT' }) }// 在每個(gè) INCREMENT_ASYNC action 調(diào)用后,派生一個(gè)新的 incrementAsync 任務(wù) export default function* watchIncrementAsync() {yield* takeEvery('INCREMENT_ASYNC', incrementAsync) }第四步:組件中調(diào)用
this.dispatch({type: 'INCREMENT_ASYNC'})redux-saga 基于 Generator 有很多高級(jí)的特性, 如:
基于 take Effect 實(shí)現(xiàn)更自由的任務(wù)編排
fork 和 cancel 實(shí)現(xiàn)非阻塞任務(wù)
并行任何和 race 任務(wù)
saga 組合 ,yield* saga
因篇幅有限,這部分內(nèi)容在下一篇講解
總結(jié)
以上是生活随笔為你收集整理的精益 React 学习指南 (Lean React)- 3.4 掌控 redux 异步的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 避免每个类中都初始化日志类
- 下一篇: 新JSON绑定库JSON-B发布公开预览