Redux-Saga 初識和總結
本文於2017年4月10日發表於掘金專欄Redux-Saga 初識和總結,現轉移至知乎專欄。
進入正文前的小提示:本文中包含兩個相關例子,若是首次接觸redux-saga者,為了便於快速理解可跳過第二個例子(Counter的例子),實踐文中的兩個小例子後再看此文效果更佳。
一、Redux-Saga介紹
redux-saga 是一個旨在於在React/Redux應用中更好、更易地解決非同步操作(action)的庫。主要模塊是 saga 會像一個分散的支線在你的應用中單獨負責解決非同步的action(類似於後台運行的進程)。詳細移步:Redux-saga
redux-saga相當於在Redux原有數據流中多了一層,對Action進行監聽,捕獲到監聽的Action後可以派生一個新的任務對state進行維護(當然也不是必須要改變State,可以根據項目的需求設計),通過更改的state驅動View的變更。圖如下所示:
用過redux-thunk的人會發現,redux-saga 其實和redux-thunk做的事情類似,都是可以處理非同步操作和協調複雜的dispatch。不同點在於:
- Sagas 是通過 Generator 函數來創建的,意味著可以用同步的方式寫非同步的代碼;
- Thunks 是在 action 被創建時才調用,Sagas 在應用啟動時就開始調用,監聽action 並做相應處理; (通過創建 Sagas 將所有的非同步操作邏輯收集在一個地方集中處理)
- 啟動的任務可以在任何時候通過手動取消,也可以把任務和其他的 Effects 放到 race 方法里可以自動取消;
二、入門demo
redux-saga-beginner-tutorial
$ git clone https://github.com/HelianXJ/redux-saga-beginner-tutorial.gitn$ git checkout redux-tool-saga // 切到有redux tool的分支配合chorme 的 Redux DevTools 工具查看邏輯更清晰nn$ npm i //下載依賴n$ npm run hello //先看項目文件中的hello sagasn
啟動server成功後view-on: http://172.22.32.14:9966/
可看到如下界面,一個簡單的例子,點擊say hello按鈕展示 hello,點擊say goodbye按鈕展示goodbye。可注意看右邊欄的Action變化和console控制台的輸出。
sagas.js 關鍵代碼
import { takeEvery } from redux-saga;nnexport function* helloSaga() {n console.log(Hello Sagas!);n}nnexport default function* watchIncrementAsync() {n yield* takeEvery(SAY_HELLO, helloSaga);n}n
這裡sagas創建了一個watchIncrementAsync 監聽SAY_HELLO的Action,派生一個新的任務——在控制台列印出「Hello Sagas!」通過這例子可以理解redux-saga大致做的事情。
該項目中還有一個計數器的簡單例子。
$ npm start //即可查看Counter的例子n
sagas.js關鍵代碼
// 一個工具函數:返回一個 Promise,這個 Promise 將在 1 秒後 resolvenexport const delay = ms => new Promise(resolve => setTimeout(resolve, ms))nn// Our worker Saga: 將非同步執行 increment 任務nexport function* incrementAsync() {n yield delay(1000);n yield put({ type: INCREMENT });n}nn// Our watcher Saga: 在每個 INCREMENT_ASYNC action 調用後,派生一個新的 incrementAsync 任務nexport default function* watchIncrementAsync() {n yield* takeEvery(INCREMENT_ASYNC, incrementAsync);n}n
計數器例子的單元測試 sagas.spec.js 關鍵代碼
import test from tape;nimport { put, call } from redux-saga/effectsnimport { incrementAsync, delay } from ./sagasnntest(incrementAsync Saga test, (assert) => {n const gen = incrementAsync()nn assert.deepEqual(n gen.next().value,n call(delay, 1000),n incrementAsync Saga must call delay(1000)n )nn assert.deepEqual(n gen.next().value,n put({type: INCREMENT}),n incrementAsync Saga must dispatch an INCREMENT actionn )nn assert.deepEqual(n gen.next(),n { done: true, value: undefined },n incrementAsync Saga must be donen )nn assert.end()n});n
由於redux-saga是用ES6的Generators實現非同步,incrementAsync 是一個 Generator 函數,所以當我們在 middleware 之外運行它,會返回一個易預見的遍歷器對象, 這一點應用在單元測試中更容易寫unit。
redux-saga能做的不只是可以做以上例子的事情。
實際上 redux-saga 所有的任務都通用 yield Effects 來完成。它為各項任務提供了各種 Effect 創建器,可以是:
- 調用一個非同步函數;
- 發起一個 action 到 Store;
- 啟動一個後台任務或者等待一個滿足某些條件的未來的 action。
三、redux-sagas的使用
- 組合sagas (yield Sagas) —— 實際上和redux-thunk 的dispatch 一個action類似
function* fetchPosts() {n yield put( actions.requestPosts() )n const products = yield call(fetchApi, /products)n yield put( actions.receivePosts(products) )n}nnfunction* watchFetch() {n while ( yield take(FETCH_POSTS) ) {n yield call(fetchPosts) // waits for the fetchPosts task to terminaten }n}n
當 yield 一個 call 至 Generator,Saga 將等待 Generator 處理結束, 然後以返回的值恢復執行
- 任務取消 —— 一旦任務被 fork,可以使用 yield cancel(task) 來中止任務執行。取消正在運行的任務,將拋出 SagaCancellationException 錯誤。
- 同時執行多個任務
const [users, repos] = yield [n call(fetch, /users),n call(fetch, /repos)n ]n
- 使用輔助函數管理 Effects 之間的並發。
function* takeEvery(pattern, saga, ...args) {n while(true) const action = yield take(pattern)n yield fork(saga, ...args.concat(action))n }n}n
三、Redux-Saga優點
- 流程拆分更細,非同步的action 以及特殊要求的action(更複雜的action)都在sagas中做統一處理,流程邏輯更清晰,模塊更乾淨;
- 以用同步的方式寫非同步代碼,可以做一些async 函數做不到的事情 (無阻塞並發、取消請求)
- 能容易地測試 Generator 里所有的業務邏輯
- 可以通過監聽Action 來進行前端的打點日誌記錄,減少侵入式打點對代碼的侵入程度
四、帶來的問題和可接受性
- action 任務拆分更細,原有流程上相當於多了一個環節。對開發者的設計和抽象拆分能力更有要求,代碼複雜性也有所增加。
- 非同步請求相關的問題較難調試排查
推薦閱讀:
※如何規模化React應用
※redux middleware 詳解
※React+AntD後台管理系統解決方案(補)
※【React/Redux/Router/Immutable】React最佳實踐的正確食用姿勢
※如何在非 React 項目中使用 Redux