Redux-saga使用心得總結(包含樣例代碼),git
本文的原文地址:原文地址github
本文的樣例代碼地址:樣例代碼地址 ,歡迎starweb
最近將項目中redux的中間件,從redux-thunk替換成了redux-saga,作個筆記總結一下redux-saga的使用心得,閱讀本文須要瞭解什麼是redux,redux中間件的用處是什麼?若是弄懂上述兩個概念,就能夠繼續閱讀本文。ajax
- redux-thunk處理反作用的缺點
- redux-saga寫一個hellosaga
- redux-saga的使用技術細節
- redux-saga實現一個登錄和列表樣例
redux中的數據流大體是:編程
UI—————>action(plain)—————>reducer——————>state——————>UIjson
redux是遵循函數式編程的規則,上述的數據流中,action是一個原始js對象(plain object)且reducer是一個純函數,對於同步且沒有反作用的操做,上述的數據流起到能夠管理數據,從而控制視圖層更新的目的。redux
可是若是存在反作用,好比ajax異步請求等等,那麼應該怎麼作?api
若是存在反作用函數,那麼咱們須要首先處理反作用函數,而後生成原始的js對象。如何處理反作用操做,在redux中選擇在發出action,到reducer處理函數之間使用中間件處理反作用。promise
redux增長中間件處理反作用後的數據流大體以下:babel
UI——>action(side function)—>middleware—>action(plain)—>reducer—>state—>UI
在有反作用的action和原始的action之間增長中間件處理,從圖中咱們也能夠看出,中間件的做用就是:
轉換異步操做,生成原始的action,這樣,reducer函數就能處理相應的action,從而改變state,更新UI。
在redux中,thunk是redux做者給出的中間件,實現極爲簡單,10多行代碼:
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的類型,若是action是函數,就調用這個函數,調用的步驟爲:
action(dispatch, getState, extraArgument);
發現實參爲dispatch和getState,所以咱們在定義action爲thunk函數是,通常形參爲dispatch和getState。
hunk的缺點也是很明顯的,thunk僅僅作了執行這個函數,並不在意函數主體內是什麼,也就是說thunk使
得redux能夠接受函數做爲action,可是函數的內部能夠多種多樣。好比下面是一個獲取商品列表的異步操做所對應的action:
export default ()=>(dispatch)=>{ fetch('/api/goodList',{ //fecth返回的是一個promise method: 'get', dataType: 'json', }).then(function(json){ var json=JSON.parse(json); if(json.msg==200){ dispatch({type:'init',data:json.data}); } },function(error){ console.log(error); }); };
從這個具備反作用的action中,咱們能夠看出,函數內部極爲複雜。若是須要爲每個異步操做都如此定義一個action,顯然action不易維護。
action不易維護的緣由:
跟redux-thunk,redux-saga是控制執行的generator,在redux-saga中action是原始的js對象,把全部的異步反作用操做放在了saga函數裏面。這樣既統一了action的形式,又使得異步操做集中能夠被集中處理。
redux-saga是經過genetator實現的,若是不支持generator須要經過插件babel-polyfill轉義。咱們接着來實現一個輸出hellosaga的例子。
export function * helloSaga() { console.log('Hello Sagas!'); }
在main.js中:
import { createStore, applyMiddleware } from 'redux' import createSagaMiddleware from 'redux-saga' import { helloSaga } from './sagas' const sagaMiddleware=createSagaMiddleware(); const store = createStore( reducer, applyMiddleware(sagaMiddleware) ); sagaMiddleware.run(helloSaga); //會輸出Hello, Sagas!
和調用redux的其餘中間件同樣,若是想使用redux-saga中間件,那麼只要在applyMiddleware中調用一個createSagaMiddleware的實例。惟一不一樣的是須要調用run方法使得generator能夠開始執行。
redux-saga除了上述的action統1、能夠集中處理異步操做等優勢外,redux-saga中使用聲明式的Effect以及提供了更加細膩的控制流。
redux-saga中最大的特色就是提供了聲明式的Effect,聲明式的Effect使得redux-saga監聽原始js對象形式的action,而且能夠方便單元測試,咱們一一來看。
首先來看redux-thunk的大致過程:
action1(side function)—>redux-thunk監聽—>執行相應的有反作用的方法—>action2(plain object)
轉化到action2是一個原始js對象形式的action,而後執行reducer函數就會更新store中的state。
而redux-saga的大致過程以下:
action1(plain object)——>redux-saga監聽—>執行相應的Effect方法——>返回描述對象—>恢復執行異步和反作用函數—>action2(plain object)
對比redux-thunk咱們發現,redux-saga中監聽到了原始js對象action,並不會立刻執行反作用操做,會先經過Effect方法將其轉化成一個描述對象,而後再將描述對象,做爲標識,再恢復執行反作用函數。
經過使用Effect類函數,能夠方便單元測試,咱們不須要測試反作用函數的返回結果。只須要比較執行Effect方法後返回的描述對象,與咱們所指望的描述對象是否相同便可。
舉例來講,call方法是一個Effect類方法:
import { call } from 'redux-saga/effects' function* fetchProducts() { const products = yield call(Api.fetch, '/products') // ... }
上述代碼中,好比咱們須要測試Api.fetch返回的結果是否符合預期,經過調用call方法,返回一個描述對象。這個描述對象包含了所須要調用的方法和執行方法時的實際參數,咱們認爲只要描述對象相同,也就是說只要調用的方法和執行該方法時的實際參數相同,就認爲最後執行的結果確定是知足預期的,這樣能夠方便的進行單元測試,不須要模擬Api.fetch函數的具體返回結果。
import { call } from 'redux-saga/effects' import Api from '...' const iterator = fetchProducts() // expects a call instruction assert.deepEqual( iterator.next().value, call(Api.fetch, '/products'), "fetchProducts should yield an Effect call(Api.fetch, './products')" )
下面來介紹幾個Effect中經常使用的幾個方法,從低階的API,好比take,call(apply),fork,put,select等,以及高階API,好比takeEvery和takeLatest等,從而加深對redux-saga用法的認識(這節可能比較生澀,在第三章中會結合具體的實例來分析,本小節先對各類Effect有一個初步的瞭解)。
引入:
import {take,call,put,select,fork,takeEvery,takeLatest} from 'redux-saga/effects'
take這個方法,是用來監聽action,返回的是監聽到的action對象。好比:
const loginAction = { type:'login' }
在UI Component中dispatch一個action:
dispatch(loginAction)
在saga中使用:
const action = yield take('login');
能夠監聽到UI傳遞到中間件的Action,上述take方法的返回,就是dipath的原始對象。一旦監聽到login動做,返回的action爲:
{ type:'login' }
call和apply方法與js中的call和apply類似,咱們以call方法爲例:
call(fn, ...args)
call方法調用fn,參數爲args,返回一個描述對象。不過這裏call方法傳入的函數fn能夠是普通函數,也能夠是generator。call方法應用很普遍,在redux-saga中使用異步請求等經常使用call方法來實現。
yield call(fetch,'/userInfo',username)
在前面提到,redux-saga作爲中間件,工做流是這樣的:
UI——>action1————>redux-saga中間件————>action2————>reducer..
從工做流中,咱們發現redux-saga執行完反作用函數後,必須發出action,而後這個action被reducer監聽,從而達到更新state的目的。相應的這裏的put對應與redux中的dispatch,工做流程圖以下:
從圖中能夠看出redux-saga執行反作用方法轉化action時,put這個Effect方法跟redux原始的dispatch類似,都是能夠發出action,且發出的action都會被reducer監聽到。put的使用方法:
yield put({type:'login'})
put方法與redux中的dispatch相對應,一樣的若是咱們想在中間件中獲取state,那麼須要使用select。select方法對應的是redux中的getState,用戶獲取store中的state,使用方法:
const state= yield select()
fork方法在第三章的實例中會詳細的介紹,這裏先提一筆,fork方法至關於web work,fork方法不會阻塞主線程,在非阻塞調用中十分有用。
takeEvery和takeLatest用於監聽相應的動做並執行相應的方法,是構建在take和fork上面的高階api,好比要監聽login動做,好用takeEvery方法能夠:
takeEvery('login',loginFunc)
takeEvery監聽到login的動做,就會執行loginFunc方法,除此以外,takeEvery能夠同時監聽到多個相同的action。
takeLatest方法跟takeEvery是相同方式調用:
takeLatest('login',loginFunc)
與takeLatest不一樣的是,takeLatest是會監聽執行最近的那個被觸發的action。
接着咱們來實現一個redux-saga樣例,存在一個登錄頁,登錄成功後,顯示列表頁,而且,在列表頁,可
以點擊登出,返回到登錄頁。例子的最終展現效果以下:
樣例的功能流程圖爲:
接着咱們按照上述的流程來一步步的實現所對應的功能。
登錄頁的功能包括
用戶名輸入框和密碼框onchange時觸發的函數爲:
changeUsername:(e)=>{ dispatch({type:'CHANGE_USERNAME',value:e.target.value}); }, changePassword:(e)=>{ dispatch({type:'CHANGE_PASSWORD',value:e.target.value}); }
在函數中最後會dispatch兩個action:CHANGE_USERNAME和CHANGE_PASSWORD。
在saga.js文件中監聽這兩個方法並執行反作用函數,最後put發出轉化後的action,給reducer函數調用:
function * watchUsername(){ while(true){ const action= yield take('CHANGE_USERNAME'); yield put({type:'change_username', value:action.value}); } } function * watchPassword(){ while(true){ const action=yield take('CHANGE_PASSWORD'); yield put({type:'change_password', value:action.value}); } }
最後在reducer中接收到redux-saga的put方法傳遞過來的action:change_username和change_password,而後更新state。
在UI中發出的登錄事件爲:
toLoginIn:(username,password)=>{ dispatch({type:'TO_LOGIN_IN',username,password}); }
登錄事件的action爲:TO_LOGIN_IN.對於登入事件的處理函數爲:
while(true){ //監聽登入事件 const action1=yield take('TO_LOGIN_IN'); const res=yield call(fetchSmart,'/login',{ method:'POST', body:JSON.stringify({ username:action1.username, password:action1.password }) if(res){ put({type:'to_login_in'}); } });
在上述的處理函數中,首先監聽原始動做提取出傳遞來的用戶名和密碼,而後請求是否登錄成功,若是登錄成功有返回值,則執行put的action:to_login_in.
登錄成功後的頁面功能包括:
import {delay} from 'redux-saga'; function * getList(){ try { yield delay(3000); const res = yield call(fetchSmart,'/list',{ method:'POST', body:JSON.stringify({}) }); yield put({type:'update_list',list:res.data.activityList}); } catch(error) { yield put({type:'update_list_error', error}); } }
爲了演示請求過程,咱們在本地mock,經過redux-saga的工具函數delay,delay的功能至關於延遲xx秒,由於真實的請求存在延遲,所以能夠用delay在本地模擬真實場景下的請求延遲。
const action2=yield take('TO_LOGIN_OUT'); yield put({type:'to_login_out'});
與登入類似,登出的功能從UI處接受action:TO_LOGIN_OUT,而後轉發action:to_login_out
function * getList(){ try { yield delay(3000); const res = yield call(fetchSmart,'/list',{ method:'POST', body:JSON.stringify({}) }); yield put({type:'update_list',list:res.data.activityList}); } catch(error) { yield put({type:'update_list_error', error}); } } function * watchIsLogin(){ while(true){ //監聽登入事件 const action1=yield take('TO_LOGIN_IN'); const res=yield call(fetchSmart,'/login',{ method:'POST', body:JSON.stringify({ username:action1.username, password:action1.password }) }); //根據返回的狀態碼判斷登錄是否成功 if(res.status===10000){ yield put({type:'to_login_in'}); //登錄成功後獲取首頁的活動列表 yield call(getList); } //監聽登出事件 const action2=yield take('TO_LOGIN_OUT'); yield put({type:'to_login_out'}); } }
經過請求狀態碼判斷登入是否成功,在登錄成功後,能夠經過:
yield call(getList)
的方式調用獲取活動列表的函數getList。這樣咋一看沒有什麼問題,可是注意call方法調用是會阻塞主線程的,具體來講:
在延遲期間的登出操做會被忽略。
用框圖能夠更清楚的分析:
call方法調用阻塞主線程的具體效果以下動圖所示:
白屏時爲請求列表的等待時間,在此時,咱們點擊登出按鈕,沒法響應登出功能,直到請求列表成功,展現列表信息後,點擊登出按鈕纔有相應的登出功能。也就是說call方法阻塞了主線程。
咱們在第二章中,介紹了fork方法能夠相似與web work,fork方法不會阻塞主線程。應用於上述例子,咱們能夠將:
yield call(getList)
修改成:
yield fork(getList)
這樣展現的結果爲:
經過fork方法不會阻塞主線程,在白屏時點擊登出,能夠馬上響應登出功能,從而返回登錄頁面。
經過上述章節,咱們能夠歸納出redux-saga作爲redux中間件的所有優勢: