Redux-saga使用心得總結(包含樣例程式碼),
本文的原文地址:原文地址
本文的樣例程式碼地址:樣例程式碼地址 ,歡迎star
最近將專案中redux的中介軟體,從redux-thunk替換成了redux-saga,做個筆記總結一下redux-saga的使用心得,閱讀本文需要了解什麼是redux,redux中介軟體的用處是什麼?如果弄懂上述兩個概念,就可以繼續閱讀本文。
- redux-thunk處理副作用的缺點
- redux-saga寫一個hellosaga
- redux-saga的使用技術細節
- redux-saga實現一個登陸和列表樣例
1.redux-thunk處理副作用的缺點
(1)redux的副作用處理
redux中的資料流大致是:
UI—————>action(plain)—————>reducer——————>state——————>UI
redux是遵循函數語言程式設計的規則,上述的資料流中,action是一個原始js物件(plain object)且reducer是一個純函式,對於同步且沒有副作用的操作,上述的資料流起到可以管理資料,從而控制檢視層更新的目的。
但是如果存在副作用,比如ajax非同步請求等等,那麼應該怎麼做?
如果存在副作用函式,那麼我們需要首先處理副作用函式,然後生成原始的js物件。如何處理副作用操作,在redux中選擇在發出action,到reducer處理函式之間使用中介軟體處理副作用。
redux增加中介軟體處理副作用後的資料流大致如下:
UI——>action(side function)—>middleware—>action(plain)—>reducer—>state—>UI
在有副作用的action和原始的action之間增加中介軟體處理,從圖中我們也可以看出,中介軟體的作用就是:
轉換非同步操作,生成原始的action,這樣,reducer函式就能處理相應的action,從而改變state,更新UI。
(2)redux-thunk
在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。
(3)redux-thunk的缺點
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不易維護的原因:
- action的形式不統一
- 就是非同步操作太為分散,分散在了各個action中
2.redux-saga寫一個hellosaga
跟redux-thunk,redux-saga是控制執行的generator,在redux-saga中action是原始的js物件,把所有的非同步副作用操作放在了saga函式裡面。這樣既統一了action的形式,又使得非同步操作集中可以被集中處理。
redux-saga是通過genetator實現的,如果不支援generator需要通過外掛babel-polyfill轉義。我們接著來實現一個輸出hellosaga的例子。
(1)建立一個helloSaga.js檔案
export function * helloSaga() {
console.log('Hello Sagas!');
}
複製程式碼
(2)在redux中使用redux-saga中介軟體
在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可以開始執行。
3.redux-saga的使用技術細節
redux-saga除了上述的action統一、可以集中處理非同步操作等優點外,redux-saga中使用宣告式的Effect以及提供了更加細膩的控制流。
(1)宣告式的Effect
redux-saga中最大的特點就是提供了宣告式的Effect,宣告式的Effect使得redux-saga監聽原始js物件形式的action,並且可以方便單元測試,我們一一來看。
- 首先,在redux-saga中提供了一系列的api,比如take、put、all、select等API ,在redux-saga中將這一系列的api都定義為Effect。這些Effect執行後,當函式resolve時返回一個描述物件,然後redux-saga中介軟體根據這個描述物件恢復執行generator中的函式。
首先來看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')"
)
複製程式碼
(2)Effect提供的具體方法
下面來介紹幾個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
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)
call和apply方法與js中的call和apply相似,我們以call方法為例:
call(fn, ...args)
複製程式碼
call方法呼叫fn,引數為args,返回一個描述物件。不過這裡call方法傳入的函式fn可以是普通函式,也可以是generator。call方法應用很廣泛,在redux-saga中使用非同步請求等常用call方法來實現。
yield call(fetch,'/userInfo',username)
複製程式碼
- put
在前面提到,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'})
複製程式碼
- select
put方法與redux中的dispatch相對應,同樣的如果我們想在中介軟體中獲取state,那麼需要使用select。select方法對應的是redux中的getState,使用者獲取store中的state,使用方法:
const state= yield select()
複製程式碼
- fork
fork方法在第三章的例項中會詳細的介紹,這裡先提一筆,fork方法相當於web work,fork方法不會阻塞主執行緒,在非阻塞呼叫中十分有用。
- takeEvery和takeLatest
takeEvery和takeLatest用於監聽相應的動作並執行相應的方法,是構建在take和fork上面的高階api,比如要監聽login動作,好用takeEvery方法可以:
takeEvery('login',loginFunc)
複製程式碼
takeEvery監聽到login的動作,就會執行loginFunc方法,除此之外,takeEvery可以同時監聽到多個相同的action。
takeLatest方法跟takeEvery是相同方式呼叫:
takeLatest('login',loginFunc)
複製程式碼
與takeLatest不同的是,takeLatest是會監聽執行最近的那個被觸發的action。
4.redux-saga實現一個登陸和列表樣例
接著我們來實現一個redux-saga樣例,存在一個登陸頁,登陸成功後,顯示列表頁,並且,在列表頁,可
以點選登出,返回到登陸頁。例子的最終展示效果如下:
樣例的功能流程圖為:
接著我們按照上述的流程來一步步的實現所對應的功能。
(1)LoginPanel(登陸頁)
登陸頁的功能包括
- 輸入時時儲存使用者名稱
- 輸入時時儲存密碼
- 點選sign in 請求判斷是否登陸成功
I)輸入時時儲存使用者名稱和密碼
使用者名稱輸入框和密碼框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。
II)監聽登陸事件判斷登陸是否成功
在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.
(2) LoginSuccess(登陸成功列表展示頁)
登陸成功後的頁面功能包括:
- 獲取列表資訊,展示列表資訊
- 登出功能,點選可以返回登陸頁面
I)獲取列表資訊
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在本地模擬真實場景下的請求延遲。
II)登出功能
const action2=yield take('TO_LOGIN_OUT');
yield put({type:'to_login_out'});
複製程式碼
與登入相似,登出的功能從UI處接受action:TO_LOGIN_OUT,然後轉發action:to_login_out
(3) 完整的實現登入登出和列表展示的程式碼
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方法之後的語句是無法執行的
-
如果call(getList)存在延遲,call(getList)之後的語句 const action2=yieldtake('TO_LOGIN_OUT')在call方法返回結果之前無法執行
-
在延遲期間的登出操作會被忽略。
用框圖可以更清楚的分析:
call方法呼叫阻塞主執行緒的具體效果如下動圖所示:
白屏時為請求列表的等待時間,在此時,我們點選登出按鈕,無法響應登出功能,直到請求列表成功,展示列表資訊後,點選登出按鈕才有相應的登出功能。也就是說call方法阻塞了主執行緒。
(4) 無阻塞呼叫
我們在第二章中,介紹了fork方法可以類似與web work,fork方法不會阻塞主執行緒。應用於上述例子,我們可以將:
yield call(getList)
複製程式碼
修改為:
yield fork(getList)
複製程式碼
這樣展示的結果為:
通過fork方法不會阻塞主執行緒,在白屏時點選登出,可以立刻響應登出功能,從而返回登陸頁面。
5.總結
通過上述章節,我們可以概括出redux-saga做為redux中介軟體的全部優點:
-
統一action的形式,在redux-saga中,從UI中dispatch的action為原始物件
-
集中處理非同步等存在副作用的邏輯
-
通過轉化effects函式,可以方便進行單元測試
-
完善和嚴謹的流程控制,可以較為清晰的控制複雜的邏輯。