在學習redux-saga之前,我們必須瞭解ES6 Generators [ES6 Generators]
介紹
中介軟體:將具體業務和底層邏輯解耦的元件。redux中,中介軟體就是一個函式,對store.dispatch方法進行了改造,在發出 Action 和執行 Reducer 這兩步之間,新增了其他功能。
redux應用中最常用的兩種非同步流處理方式: redux-thunk,redux-saga
redux-saga 是一個用於管理 Redux 應用非同步操作(Side Effects。譯註:直譯成 “副作用” 不太通順,所以這裡譯為 “非同步操作” 更好理解)的中介軟體(又稱非同步 action)。 redux-saga 通過建立 Sagas 將所有的非同步操作邏輯收集在一個地方集中處理,可以用來代替 redux-thunk 中介軟體。
它具有如下特性:
集中處理 redux 副作用問題。
被實現為 generator 。
類 redux-thunk 中介軟體。
watch/worker(監聽->執行) 的工作形式。
如何使用
- 建立與執行
- 測試(遍歷 generator 並對它的值(
gen.next() // => { done: ..., value: ... }
)做 deepEqual 測試)
redux-saga [官方文件]有詳細的使用說明
redux-saga [中文文件]
概念
使用 Saga 輔助函式
(常用)
- Middleware API
- createSagaMiddleware(...sagas)
- middleware.run(saga, ...args)
Saga Helpers
- takeEvery(pattern, saga, ...args) :接受Action,並且觸發某個方法(一個 Generator function) 例項同時啟動
- takeLatest(pattern, saga, ..args) :多次觸發,去最後一次
Effect creators
- take(pattern) :建立一條 Effect 描述資訊,指示 middleware 等待 Store 上指定的 action。 Generator 會暫停,直到一個與 pattern 匹配的 action 被髮起。
- put(action) : put(action) 用於觸發 action,功能上類似於dispatch。
- fork(fn, ...args) :建立一條 Effect 描述資訊,指示 middleware 以 無阻塞呼叫 方式執行 fn。fork 類似於 call,可以用來呼叫普通函式和 Generator 函式。但 fork 的呼叫是無阻塞的,在等待 fn 返回結果時,middleware 不會暫停 Generator。 相反,一旦 fn 被呼叫,Generator 立即恢復執行。 fork 與 race 類似,是一箇中心化的 Effect,管理 Sagas 間的併發。
- call(fn, ...args) :cal阻塞型呼叫,有阻塞地呼叫 saga 或者返回 promise 的函式。
- join(task): 建立一個效果描述,指示中介軟體等待先前分叉的任務的結果。
- cancel(task): 是一個無阻塞 Effect。也就是說,Generator 將在取消異常被丟擲後立即恢復。
- select(selector, ...args) : 作用和 redux thunk 中的 getState 相同。
const id = yield select(state => state.id);複製程式碼
Effect combinators
- race(effects) :rece Effect提供了一個方法,在多個Effects之間觸發一個競賽。在race Effect中,所有參與競賽的任務,除了優勝者,其他任務都會被取消
Interfaces
- Task :Task 介面指定了通過
fork
,middleware.run
或runSaga
執行 Saga 的結果。
- Task :Task 介面指定了通過
redux-saga API詳解可以檢視 [API 參考]
宣告式 Effects
Effect 是一個簡單的物件,該物件包含了一些給 middleware 解釋執行的資訊。可以通過使用 effects API 如 fork,call,take,put,cancel
等來建立 Effect。( redux-saga [API 參考])
觸發一個action 到 reducer的過程中,如 yield call(delay, 1000)
即 yield 了下面的物件,call 建立了一條描述結果的資訊,然後,redux-saga middleware
將確保執行這些指令並將指令的結果返回給 Generator
從 Saga 內觸發非同步操作(Side Effect)總是由 yield 一些宣告式的 Effect 來完成的 (你也可以直接 yield Promise,但是這會讓測試變得困難。使用 Effect 諸如 call 和 put,與高階 API 如 takeEvery 相結合,又有額外的易於測試的好處。
發起action
redux-saga 提供了另外一個函式 put,這個函式用於建立 dispatch Effect
-
yield put({ type: '',... })
yield關鍵字用來暫停和恢復一個生成器函式,yield關鍵字使生成器函式執行暫停,yield關鍵字後面的表示式的值返回給生成器的呼叫者。它可以被認為是一個基於生成器的版本的return關鍵字。
yield關鍵字實際返回一個IteratorResult物件,它有兩個屬性,value和done。value屬性是對yield表示式求值的結果,而done是false,表示生成器函式尚未完全完成。
錯誤處理
try/catch
語法在 Saga 中捕獲錯誤
redux-saga 是如何工作的?
Redux Saga可以理解為一個和系統互動的常駐程式,其中,Saga可簡單定義如下:
- Saga = Worker + Watcher
Sagas通過Generator函式來建立,採用 Generator 函式來 yield Effects,只會在應用啟動時呼叫,我們可以把它看作是在後臺執行的程式,Sagas監聽發起的action,然後決定基於這個action來做什麼,是發起非同步呼叫還是發起其他的action到Store,甚至是呼叫其他的Sagas
- sagas 包含3個部分,用於聯合執行任務:
Watcher/Worker 指的是一種使用兩個單獨的 Saga 來組織控制流的方式。
1. root saga:立即啟動 sagas 的唯一入口
2. Watcher: 監聽發起的 action 並在每次接收到 action 時 fork 一個 worker。
3. Worker: 處理 action 並結束它。
eg:
//動態執行 rootSaga。用於 applyMiddleware 階段之後執行 rootSaga。
middleware.run(rootSaga, ...args)
...
//Watcher/Worker
function* watcher() {
while(true) {
const action = yield take(ACTION)
yield fork(worker, action.payload)
}
}
function* worker(payload) {
// ... do some stuff
}
複製程式碼
VS redux-thunk
- 使用redux-thunk
try { dispatch({ type: LOGIN_REQUEST }); let { data } = await request.post('/login', { user, password }); await dispatch(loadUserData(data.uid)); dispatch({ type: LOGIN_SUCCESS, data }); } catch(error) { dispatch({ type: LOGIN_ERROR, error }); } } export const loadUserData = (uid) => async (dispatch) => { try { dispatch({ type: USERDATA_REQUEST }); let { data } = await request.get(`/users/${uid}`); dispatch({ type: USERDATA_SUCCESS, data }); } catch(error) { dispatch({ type: USERDATA_ERROR, error }); } } 複製程式碼
- 使用redux-saga
export function* loginSaga() { while(true) { const { user, pass } = yield take(LOGIN_REQUEST) //等待 Store 上指定的 action LOGIN_REQUEST try { let { data } = yield call(request.post, '/login', { user, pass }); //阻塞,請求後臺資料 yield fork(loadUserData, data.uid); //非阻塞執行loadUserData yield put({ type: LOGIN_SUCCESS, data }); //發起一個action,類似於dispatch } catch(error) { yield put({ type: LOGIN_ERROR, error }); } } } export function* loadUserData(uid) { try { yield put({ type: USERDATA_REQUEST }); let { data } = yield call(request.get, `/users/${uid}`); yield put({ type: USERDATA_SUCCESS, data }); } catch(error) { yield put({ type: USERDATA_ERROR, error }); } } 複製程式碼
- 相比Redux Thunk,使用Redux Saga有幾處明顯的變化:
1. 在元件中,不再dispatch(action creator),而是dispatch(pure action)
2. 元件中不再關注由誰來處理當前action,action經由root saga分發
3. 具體業務處理方法中,通過提供的call/put等幫助方法,宣告式的進行方法呼叫
4. 使用ES6 Generator語法,簡化非同步程式碼語法 5. redux-saga 將非同步任務進行了集中處理,且方便測試。
結論
redux-saga的使用更多的是根據個人需求與習慣
redux-saga 的優點:
1. 宣告式 Effects:所有的操作以JavaScript物件的方式被 yield,並被 middleware 執行。使得在 saga 內部測試變得更加容易,可以通過簡單地遍歷 Generator 並在 yield 後的成功值上面做一個 deepEqual 測試。
2. 高階的非同步控制流以及併發管理:可以使用簡單的同步方式描述非同步流,並通過 fork 實現併發任務。
3. 架構上的優勢:將所有的非同步流程控制都移入到了 sagas,UI 元件不用執行業務邏輯,只需 dispatch action 就行,增強元件複用性。
目前在專案實踐中遇到的一些問題:
1. redux-saga 不強迫我們捕獲異常,這往往會造成異常發生時難以發現原因。因此,一個良好的習慣是,相信任何一個過程都有可能發生異常。
2. generator 的除錯環境比較糟糕,babel 的 source-map 經常錯位,經常要手動加 debugger 來除錯。
4. 在action的定義上要謹慎,避免action在saga和reducer之間重複觸發,造成死迴圈