作者介紹:羅雪婧,美團點評前端工程師,3年 Web 前端開發經驗,現在是美團點評點餐團隊的一員。
一、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
$ git clone https://github.com/HelianXJ/redux-saga-beginner-tutorial.git
$ git checkout redux-tool-saga // 切到有redux tool的分支配合chorme 的 Redux DevTools 工具檢視邏輯更清晰
$ npm i //下載依賴
$ npm run hello //先看專案檔案中的hello sagas複製程式碼
啟動server成功後view-on: http://172.22.32.14:9966/
可看到如下介面,一個簡單的例子,點選say hello按鈕展示 hello,點選say goodbye按鈕展示goodbye。可注意看右邊欄的Action變化和console控制檯的輸出。
sagas.js 關鍵程式碼
import { takeEvery } from 'redux-saga';
export function* helloSaga() {
console.log('Hello Sagas!');
}
export default function* watchIncrementAsync() {
yield* takeEvery('SAY_HELLO', helloSaga);
}複製程式碼
這裡sagas建立了一個watchIncrementAsync 監聽SAY_HELLO的Action,派生一個新的任務——在控制檯列印出“Hello Sagas!”通過這例子可以理解redux-saga大致做的事情。
該專案中還有一個計數器的簡單例子。
$ npm start //即可檢視Counter的例子複製程式碼
sagas.js關鍵程式碼
// 一個工具函式:返回一個 Promise,這個 Promise 將在 1 秒後 resolve
export const delay = ms => new Promise(resolve => setTimeout(resolve, ms))
// Our worker Saga: 將非同步執行 increment 任務
export function* incrementAsync() {
yield delay(1000);
yield put({ type: 'INCREMENT' });
}
// Our watcher Saga: 在每個 INCREMENT_ASYNC action 呼叫後,派生一個新的 incrementAsync 任務
export default function* watchIncrementAsync() {
yield* takeEvery('INCREMENT_ASYNC', incrementAsync);
}複製程式碼
計數器例子的單元測試 sagas.spec.js 關鍵程式碼
import test from 'tape';
import { put, call } from 'redux-saga/effects'
import { incrementAsync, delay } from './sagas'
test('incrementAsync Saga test', (assert) => {
const gen = incrementAsync()
assert.deepEqual(
gen.next().value,
call(delay, 1000),
'incrementAsync Saga must call delay(1000)'
)
assert.deepEqual(
gen.next().value,
put({type: 'INCREMENT'}),
'incrementAsync Saga must dispatch an INCREMENT action'
)
assert.deepEqual(
gen.next(),
{ done: true, value: undefined },
'incrementAsync Saga must be done'
)
assert.end()
});複製程式碼
由於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() {
yield put( actions.requestPosts() )
const products = yield call(fetchApi, '/products')
yield put( actions.receivePosts(products) )
}
function* watchFetch() {
while ( yield take(FETCH_POSTS) ) {
yield call(fetchPosts) // waits for the fetchPosts task to terminate
}
}複製程式碼
當 yield 一個 call 至 Generator,Saga 將等待 Generator 處理結束, 然後以返回的值恢復執行
- 任務取消 —— 一旦任務被 fork,可以使用 yield cancel(task) 來中止任務執行。取消正在執行的任務,將丟擲 SagaCancellationException 錯誤。
- 同時執行多個任務
const [users, repos] = yield [
call(fetch, '/users'),
call(fetch, '/repos')
]複製程式碼
- 使用輔助函式管理 Effects 之間的併發。
function* takeEvery(pattern, saga, ...args) {
while(true) const action = yield take(pattern)
yield fork(saga, ...args.concat(action))
}
}複製程式碼
三、Redux-Saga優點
- 流程拆分更細,非同步的action 以及特殊要求的action(更復雜的action)都在sagas中做統一處理,流程邏輯更清晰,模組更乾淨;
- 以用同步的方式寫非同步程式碼,可以做一些async 函式做不到的事情 (無阻塞併發、取消請求)
- 能容易地測試 Generator 裡所有的業務邏輯
- 可以通過監聽Action 來進行前端的打點日誌記錄,減少侵入式打點對程式碼的侵入程度
四、帶來的問題和可接受性
- action 任務拆分更細,原有流程上相當於多了一個環節。對開發者的設計和抽象拆分能力更有要求,程式碼複雜性也有所增加。
- 非同步請求相關的問題較難除錯排查