[Redux] redux-saga 筆記
觀念
- 在 redux-saga 中,當在
yield
後接上Promise
時,middleware 會把 Saga 停住,直到這個 Promise 被 resolve 後才繼續執行。 - 一旦 Promise 被 resolve 之後,middleware 會讓 Saga 繼續執行到下一次的 yield 才停住。
- 在 redux-saga 中會從 Generator 中產生(yield)許多的 JavaScript 物件,這些物件包含了指示,因此我們把這樣的物件稱作「effect」,這些物件的指示會告訴 middleware 現在要執行哪些動作(例如,執行非同步的函式、向 store 發出 dispatch 等等)。
- 在 redux-saga 中有許多不同的 effect(例如,
put
),effect 是一個帶有指示的物件,需要透過 middleware 來完成,當 middleware 收到 Saga 產生(yield)的 effect 時,Saga 會停住,和 Promise 的情況相似,需要直到這個 effect 被完成後才會繼續。也就是說,不論是put
或call
他們都沒有真正去執行 dispatch 或非同步的呼叫,他們只是回傳一個 JavaScript 物件: - Saga 可以透過很多不同的形式產生 effect,其中最簡單的一中方式就是 Promise。
put({ type: 'INCREMENT' }); // => { PUT: {type: 'INCREMENT'} }
call(delay, 1000); // => { CALL: {fn: delay, args: [1000]}}
- 真正做事的地點是在 middleware 中,它會根據 effect 的類型來決定要如何完成這個 effect。如果 effect 是
PUT
的話,那麼他會 dispatch 一個 action 到 store 中;如果 effect 是CALL
,那麼就會呼叫被給予的函式。
安裝與設定
安裝 redux-saga:
$ npm install redux-saga
建立一支 sagas.js
,在裡面定義好事件:
// sagas.js
import { delay } from 'redux-saga';
import { put, takeEvery, all } from 'redux-saga/effects';
export function* helloSaga() {
console.log('Hello Sagas!');
}
// 我們工作的 saga:將執行非同步的 increment task
// 延遲 1 秒後發出 type 為 'INCREMENT' 的事件
export function* incrementAsync() {
yield delay(1000);
yield put({ type: 'INCREMENT' });
}
// 我們觀察的 saga:當在 type 為 INCREMENT_ASYNC 時 就會執行 incrementAsync task
export function* watchIncrementAsync() {
yield takeEvery('INCREMENT_ASYNC', incrementAsync);
}
// 定義 rootSaga
export default function* rootSaga() {
yield all([helloSaga(), watchIncrementAsync()]);
}
在主要的 main.js
中載入 saga:
// main.js
// ...
import { createStore, applyMiddleware } from 'redux';
import createSagaMiddleware from 'redux-saga';
// ...
import rootSaga from './sagas';
const sagaMiddleware = createSagaMiddleware();
const store = createStore(reducer, applyMiddleware(sagaMiddleware));
sagaMiddleware.run(rootSaga);
const action = (type) => store.dispatch({ type });
// rest unchanged
監控特定 Action
keywords: takeEvery
, takeLatest
, take
import { takeEvery, takeLatest } from 'redux-saga/effects';
function* watchFetchData() {
yield takeLatest('FETCH_REQUESTED', fetchData);
}
takeEvery
:每次觸發 type 為FETCH_REQUESTED
的 action 時都會執行fetchData
。takeLatest
:如果多次觸發 type 為FETCH_REQUESTED
的 action 時,會以最後一次的回應為準,在之前還未完成(如,pending)的fetchData
將會被取消。
❗
takeLatest
並不表示只會送出一次的 request,而是當送出多個 request 時,會以最後一次送出的 request 所得到的回應為主,其餘的 request 一樣會被送出,但會被 cancel 掉。
另外 takeEvery
和 takeLatest
都屬於 Higher Level API,若有需要更多的流程控制,建議可以參考 [take
](#Take API) 這個 lower level API。
處理非同步事件:Declarative Effect
keywords: call
, apply
, cps
call
後面的第一個參數可以是會回傳 Promise
的方法,或者可以接一個 Generator function;前者可以從 yield 取得 Promise resolve
的內容,[後者](#什麼是 blocking call)則可以取得該 Generator function 執行結束後最後 return
的值:
const data = yield call([obj, obj.method], arg1, arg2, ...) // as if we did obj.method(arg1, arg2 ...)
const data = yield apply(obj, obj.method, [arg1, arg2, ...])
雖然我們可以直接透過下面的方式來在 Saga 中進行 AJAX 請求:
function* fetchData() {
const data = yield fetch('https://example.com');
}
但因為 fetch 是直接回傳 Promise 的緣故,這樣的方式難以進行單元測試,在 Saga 中提供了幾個 declarative effect,這些 declarative effect 會回傳的是一個帶有指示的物件,如此,我們只需在測試中撰寫兩個物件是否相同:
function* fetchData() {
const data = yield call(fetch, 'https://example.com');
}
Declarative Effects @ Redux-Saga