標籤:

徹徹底底教會你使用Redux-saga(包含樣例代碼)

徹徹底底教會你使用Redux-saga(包含樣例代碼)

21 人贊了文章

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 reduximport createSagaMiddleware from redux-sagaimport { helloSaga } from ./sagasconst 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/effectsfunction* fetchProducts() { const products = yield call(Api.fetch, /products) // ...}

上述代碼中,比如我們需要測試Api.fetch返回的結果是否符合預期,通過調用call方法,返回一個描述對象。這個描述對象包含了所需要調用的方法和執行方法時的實際參數,我們認為只要描述對象相同,也就是說只要調用的方法和執行該方法時的實際參數相同,就認為最後執行的結果肯定是滿足預期的,這樣可以方便的進行單元測試,不需要模擬Api.fetch函數的具體返回結果。

import { call } from redux-saga/effectsimport Api from ...const iterator = fetchProducts()// expects a call instructionassert.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函數,可以方便進行單元測試
  • 完善和嚴謹的流程式控制制,可以較為清晰的控制複雜的邏輯。

推薦閱讀:

Rematch源碼解析及思考
聽說你需要這樣了解 Redux
從redux-saga想到的前端多範式問題
實戰react技術棧+express前後端博客項目(0)-- 預熱一波
React + Redux + react router技術棧架構

TAG:Redux | Flux | React |