曾經前端的革新是以Ajax的出現爲分水嶺,現代應用中絕大部分頁面渲染會以異步流的方式進行。在Redux中,若是要發起異步請求,最合適的位置是在action creator中實現。但咱們以前瞭解到的action都是同步狀況,所以須要引入中間件讓action支持異步狀況,如異步action(異步請求)爲一個函數,或者利用promise來完成,或者是其餘自定義的形式等等,下面的middleware就是用來處理這些不一樣異步action(或者說成actionCreator)的.另外,在Redux社區裏還有其餘一些處理異步的中間件,它們大同小異,這裏就不一一分析了。javascript
redux-thunk 是 redux 官方文檔中用到的異步組件,實質就是一個 redux 中間件,thunk 簡單來講 就是一個封裝表達式的函數,封裝的目的是延遲執行表達式。前端
redux-thunk 是一個通用的解決方案,其核心思想是讓 action 能夠變爲一個 thunk ,這樣的話:java
異步狀況:dispatch(thunk)ajax
thunk 本質上就是一個函數,函數的參數爲 dispatch, 因此一個簡單的 thunk 異步代碼就是以下:json
this.dispatch(function (dispatch){ setTimeout(() => { dispatch({type: 'THUNK_ACTION'}) }, 1000) })
以前已經講過,這樣的設計會致使異步邏輯放在了組件中,解決辦法爲抽象出一個 asyncActionCreator, 這裏也同樣,咱們就叫 thunkActionCreator 吧,上面的例子能夠改成:redux
export function createThunkAction(payload) { return function(dispatch) { setTimeout(() => { dispatch({type: 'THUNK_ACTION', payload: payload}) }, 1000) } } // someComponent.js this.dispatch(createThunkAction(payload))
redux-thunk源碼:segmentfault
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爲函數的時候,咱們並無調用next或dispatch方法,而是返回action的調用。這裏的action即爲一個Thunk函數,以達到將dispatch和getState參數傳遞到函數內的做用。api
此時,action能夠寫成thunk形式(ThunkActionCreator):數組
function getweather(url,params){ return (dispatch,getState)=>{ fetch(url,params) .then(result=>{ dispatch({ type:'GET_WEATHER_SUCCESS', payload:result, }); }) .catch(err=>{ dispatch({ type:'GET_WEATHER_ERROR', error:err, }); }); }; }
其實 thunk
咱們已經有了處理異步的能力, 可是每次咱們要本身去手動觸發三個 action
, 工做量仍是很大的。如今 ajax
不少都會包裝爲 promise
對象,,異步請求其實都是利用promise來完成的 所以咱們能夠對與 dispatch
增長一層判斷, 使得它具備處理具備 promise
屬性的 action
的能力。promise
import {isFSA} from 'flux-standard-action'; function isPromise(val){ 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); }; }
思路:redux-promise兼容了FSA標準(瞭解FSA可參考https://segmentfault.com/a/11...),也就是說將返回的結果保存在payload中。實現過程很是容易理解,即判斷action或action.payload是否爲promise,若是是,就執行then,返回的結果再發送一次dispatch。
此時,action能夠寫成promise形式(promiseActionCreator):
//利用ES7的async和awaita語法 const fetchData=(url,params)=>fetch(url,params); async function getWeather(url,params){ const result=await fetchData(url,params); if(result.error){ return{ type:'GET_WEATHER_ERROR', error:'result.error', }; } return{ type:'GET_WEATHER_SUCCESS', payload:'result' }; }
redux-saga是redux社區一個處理異步流的後起之秀,它與上述方法最直觀的不一樣就是用generator代替了promise。的確,redux-saga是最優雅的通用解決方案,它有着靈活而強大的協程機制,能夠解決任何複雜的異步交互,具體的,放在另外一篇文章中詳細介紹。
在理想狀況下,咱們不但願經過複雜的方法去請求數據,而是但願經過以下形式一併完成在異步請求過程當中的不一樣狀態:
{ url:'/api/weather.json', params:{ city:encodeURL(city), } type:['GET_WEATHER','GET_WEATHER_SUCCESS','GET_WEATHER_ERROR'], }
能夠看到,異步請求action的格式有別於FSA。它並無使用type屬性,而使用了types屬性。在請求middleware中,會對action進行格式檢查,若存在url和types屬性,則說明這個action是一個用於發送異步請求的action。此外,並非全部請求都能攜帶參數,所以params是可選的。
const fetchMiddleware=store=>next=>action=>{ if(!action.url || !Array.isArray(action.types)){ return next(action); } const [LOADING,SUCCESS,ERROR]=action.types; next({ type: LOADING, loading: true, ...action, }); fetch(action.url,{params:action.params}) .then(result=>{ next({ type:SUCCES, loading:false, payload:result, }); }) .catch(err=>{ next({ type:ERROR, laoding:false, error:err, }); }); }
在實際場景中,咱們不但有短鏈接請求,還有輪詢請求、多異步串聯請求,或是在異步中加入同步處理的邏輯。這時咱們須要對通常異步中間件進行處理。
輪詢是長鏈接的一種實現方式,它可以在必定時間內從新啓動自身,而後再次發起請求。基於這個特性,咱們能夠在上一個中間件的基礎上再寫一個middleware,這裏命名爲redux-polling:
import setRafTimeout,{clearRafTimeout} from 'setRafTimeout'; export default ({dispatch,getState})=>next=>action{ const {poolingUrl,params,types}=action; const isPollingAction=pollingUrl&¶ms&&types; if(!isPollingAction){ return next(action); } let timeoutId=null; const startPolling=(timeout=0)=>{ timeoutId=setRafTimeout(()=>{ const pollingAction={ ...others, url:pollingUrl, timeoutId, }; dispatch(pollingAction).then(data=>{ if(data && data.interval && typeof data.interval=='number'){ startPolling(data.interval*1000); } else { console.error('pollingAction should fetch data contain interval'); } }); },timeout); }; startPolling(); } export const clearPollingTimeout=(timeId)=> { if(timeoutId){ clearRafTimeout(timeId); } };
咱們用到了raf函數,它可讓請求在必定時間內從新啓動;startPolling函數爲遞歸函數,這樣能夠,知足輪詢的請求;在API的設計上,還暴露了clearPollingTimeout方法,以便咱們在須要時手動中止輪詢。
最後,調用action來發起輪詢:
{ pollingurl:'/api/weather.json', params:{ city:encodeURL(city), }, types:[null,'GET_WEATHER-SUCCESS',null], }
對於長鏈接,還有其餘多種實現方式,最好的方式是對其總體作一次封裝,在內部實現諸如輪詢和WebSocket。
咱們能夠經過promise封裝來實現不管是否是異步請求,均可以經過promise來傳遞以達到一個統一的效果。
const sequenceMiddlware=({dispatch,getState})=>next=>action=>{ if(!Array.isArray(action)){ return next(action); } return action.reduce((result,currAction)=>{ return result.then(()=>{ return Array.isArray(currAction)? Promise.all(currAction.map(item=>dispatch(item))): dispatch(currAction); }); },Promise.resolve()); }
在構建action creator時,會傳遞一個數組,數組中每個值都是按順序執行的步驟。這裏的步驟既能夠是異步的,也能夠是同步的。在實現過程當中,咱們很是巧妙地使用了Promise.resolve()來初始化action.reduce方法,而後使用Promise.then()方法串聯起數組,達到了串聯步驟的目的。
function getCurrCity(ip){ return { url:'/api/getCurrCity.json', param: {ip}, types: [null,'GET_CITY_SUCCESS',null], } } return getWeather(cityId){ return { url:'/api/getWeatherInfo.json', param:{cityId}, types:[null,'GET_WEATHER_SUUCCESS',null], } } function loadInitData(ip){ return [ getCurrCity(ip), (dispatch,state)=>{ dispatch(getWeather(getCityIdWithState(state))); }, ]; }
這種方法利用了數組的特性,它已經覆蓋了大部分場景,固然,若是串聯過程當中有不一樣的分支,就無能爲力了。