最近項目用了dva,dva對於異步action的處理是用了redux-saga,故簡單學習了下redux-saga; 如下從 是什麼 爲何 怎麼用 三方面來了解。javascript
redux-saga 就是 redux 的一個中間件,用於更優雅地管理反作用(side effects);html
redux-saga能夠理解爲一個 和 系統交互的 常駐進程,可簡單定義: saga = Worker + Warcher
前端
Side effects are the most common way that a program interacts with the outside world (people, filesystems, other computers on networks) [from Wikipedia]java
反作用是 程序與外部世界(人、文件系統,網絡上的其餘計算機) 交互的最經常使用的方式。 映射到前端, 反作用通常指異步網絡請求。git
An effect is a plain JavaScript Object containing some instructions to be executed by the saga middleware.github
effect 是一個普通的 javascript對象,包含一些指令,這些指令最終會被 redux-saga 中間件 解釋並執行。redux
在 redux-saga 世界裏,全部的 Effect 都必須被 yield 纔會執行後端
原則上來講,全部的 yield 後面也只能跟Effect,以保證代碼的易測性。 eg:api
yield fetch(UrlMap.fetchData);
數組
應該用 call Effect : yield call(fetch, UrlMap.fetchData)
用於更優雅地管理反作用, 在前端就是異步網絡請求;本質就是爲了解決異步action的問題;
反作用轉移到單獨的saga.js中,再也不摻雜在action.js中,保持 action 的簡單純粹,又使得異步操做集中能夠被集中處理。對比redux-thunk
redux-saga 提供了豐富的 Effects,以及 sagas 的機制(全部的 saga 均可以被中斷),在處理複雜的異步問題上更順手。提供了更加細膩的控制流。
對比thunk,dispatch 的參數依然是一個純粹的 action (FSA)。
每個 saga 都是 一個 generator function,代碼能夠採用 同步書寫 的方式 去處理 異步邏輯(No Callback Hell),代碼變得更易讀。
一樣是受益於 generator function 的 saga 實現,代碼異常/請求失敗 均可以直接經過 try/catch 語法直接捕獲處理。
export function* helloSaga() {
console.log('Hello Sagas!');
}
複製代碼
import { createStore, applyMiddleware } from 'redux';
import createSagaMiddleware from 'redux-saga';
import helloSaga from './sagas'
import rootReducer from './reducers'
// 建立 saga middleware
const sagaMiddleware = createSagaMiddleware();
// 建立 store
const store = createStore(
rootReducer,
applyMiddleware(sagaMiddleware) // 注入 saga middleware
);
// 啓動 saga
sagaMiddleware.run(helloSaga);
// 省略ReactDOM.render部分的代碼...
複製代碼
這時候就能夠看到Hello Sagas!
了;
代碼分析:
line 8 :經過redux-saga提供的工廠函數 createSagaMiddleware 建立 sagaMiddleware(固然建立時,你也能夠傳遞一些可選的配置參數)。
line 11-14 : 建立 store 實例, 並注入 saga中間件。意味着:以後每次執行 store.dispatch(action),數據流都會通過 sagaMiddleware 這一道工序,進行必要的 「加工處理」(好比:發送一個異步請求)。
line 17 : 啓動 saga,調用run方法使得generator能夠開始執行,也就是執行 rootSaga。一般是程序的一些初始化操做(好比:初始化數據、註冊 action 監聽)。
三、接下來加入異步調用的流程
先看下要實現的效果:
省略UI代碼;
reducer中已有加一的處理:
...
case 'INCREMENT':
return {
...state,
count: state.count + 1
}
...
複製代碼
sagas.js:
import { all, put, takeEvery } from 'redux-saga/effects'
const delay = (ms) => new Promise(res => setTimeout(res, ms))
// worker Saga: 執行異步的 increment 任務
export function* incrementAsync() {
yield delay(1000) // middleware 拿到一個 yield 後的 Promise,暫停1s後再繼續執行
yield put({ type: 'INCREMENT' }) // 告訴 middleware 發起一個 INCREMENT 的 action。
}
// watcher Saga: 在每一個INCREMENT_ASYNC上生成一個新的incrementAsync任務
export function* watchIncrementAsync() {
yield takeEvery('INCREMENT_ASYNC', incrementAsync)
}
// 啓動saga們
export default function* rootSaga() {
yield all([
watchIncrementAsync(),
helloSaga()
])
}
複製代碼
把saga和store聯繫起來的代碼和上面類似,就是把helloSaga替換成rootSaga便可;
代碼分析:
Sagas 是被實現爲 Generator functions 的 line 2 : 建立一個
delay
函數,返回一個Promise,它在指定的毫秒數後解析。 line 5-8 : incrementAsync 這個 Saga 會暫停,直到 delay 返回的 Promise 被 resolve,即 1000ms 以後; line 6 : middleware 拿到一個 yield 後的 Promise,middleware 暫停 Saga,直到 Promise 完成。一旦 Promise 被 resolve,middleware 會恢復 Saga 接着執行,直到遇到下一個 yield。 line 7 : 這裏就是第二個yield啦,這裏的put({type: 'INCREMENT'})
就是一個Effect,Effect 是純js對象,其中包含了給 middleware 執行的指令;當 middleware 拿到被Saga yield的Effect的時候,也會暫停Saga,直到Effect 執行完成,而後Saga 會再次被恢復。 line 11-13 : 寫一個watcher saga,用redux-saga的apitakeEvery
來監聽全部的 INCREMENT_ASYNC action,並在 action 被匹配時執行 incrementAsync 任務。 line 15-18 : 有了Saga,,現添加一個rootSaga來負責啓動全部Saga,用了all
api,若是有其餘Saga都能一塊兒啓動。
line 7 返回的是一個Effect,console('Effect', put({ type: 'INCREMENT' }))
:
基於redux的數據流: 狀態決定展示,交互就是改狀態
基於redux-saga的一次完整單向數據流:
在第一次使用dva的時候,用的最多的api就是
put
和call
,有時還有用select
一、put(action)
建立一個Effect描述信息,指示 middleware 向Store dispatch一個action
至關於在 saga 中調用 store.dispatch(action)。
二、select(selector, ...args)
建立一個Effect,指示 middleware 調用提供的選擇器獲取 Store state 上的數據,即獲取狀態
三、call(fn, ...args)
建立一個Effect描述信息,指示 middleware 以args爲參數調用fn;
即執行fn(...args); 若是fn是個Generator,或者返回Promise,那麼會阻塞當前 saga 的執行,直到被調用函數 fn 返回結果,纔會執行下一步代碼。
四、take(pattern)
建立一個Effect描述信息,指示 middleware 等待 Store 上指定的 action。 Generator 會暫停(被阻塞了),直到一個與 pattern 匹配的 action 被髮起。 有種事件監聽的感受。 take的返回值是action
若是調用take而沒有參數或是'*',則全部調度的操做都匹配(例如,take()
將匹配全部操做)
能夠監聽多個,eg: yield take(['LOGOUT', 'LOGIN_ERROR'])
五、fork(fn, ...args)
建立一個Effect描述信息,指示 middleware 以 無阻塞調用 方式執行 fn fork的返回值是task
相似於 call effect,區別在於它不會阻塞當前 saga,如同後臺運行通常,會當即返回一個 task 對象。 yield fork(fn ...args)
的結果是一個 Task 對象 —— 具備一些有用方法和屬性的對象。
六、cancel(task)
建立一個Effect描述信息,針對 fork 方法返回的 task ,能夠進行取消關閉。
七、cancelled()
建立一個Effect描述信息,指示 middleware 返回 該 generator 是否已經被取消。一般你會在 finally 區塊中使用這個 Effect 來運行取消時專用的代碼。
八、takeEvery(pattern, saga, ...args)
被 dispatch 的 action 中,在匹配到 pattern 的每個 action 上派生一個 saga takeEvery 是一個使用 take 和 fork 構建的高級 API。
實現:
const takeEvery = (patternOrChannel, saga, ...args) => fork(function*() {
while (true) {
const action = yield take(patternOrChannel)
yield fork(saga, ...args.concat(action))
}
})
複製代碼
九、takeLastest(pattern, saga, ...args)
被 dispatch 的 action 中,在匹配 pattern 的每個 action 上派生一個 saga。並自動取消以前全部已經啓動但仍在執行中的 saga 任務。 takeLatest 也是一個使用 take 和 fork 構建的高級 API。 實現:
const takeLatest = (patternOrChannel, saga, ...args) => fork(function*() {
let lastTask
while (true) {
const action = yield take(patternOrChannel)
if (lastTask) {
yield cancel(lastTask) // 若是任務已經結束,cancel 則是空操做
}
lastTask = yield fork(saga, ...args.concat(action))
}
})
複製代碼
十、race([...effects])
建立一個Effect描述信息,指示 middleware 在多個 Effect 之間運行一個 race(與 Promise.race([...]) 的行爲相似)。
race能夠取到最快完成的那個結果,經常使用於請求超時
十一、all([...effects])
建立一個 Effect 描述信息,指示 middleware 並行運行多個 Effect,並等待它們所有完成。這是與標準的Promise#all相對應的 API。
也可用[...effects]
,yield 一個包含 effects 的數組,eg:
import { call } from 'redux-saga/effects'
// 正確寫法, effects 將會同步執行
const [users, repos] = yield [
call(fetch, '/users'),
call(fetch, '/repos')
];
複製代碼
generator 會被阻塞直到全部的 effects 都執行完畢,或者當一個 effect 被拒絕 (就像 Promise.all 的行爲)。
欲瞭解其餘api能夠訪問: 速查直達
對於try catch的額外補充
saga 中 call 和 fork 都是用來執行指定函數 fn,區別在於:
fork 的異步非阻塞特性更適合於在後臺運行一些不影響主流程的代碼
一、監聽將來的action —— take
先看下要實現的效果:
take的實現:
import { select, take } from 'redux-saga/effects'
function* watchAndLog() {
while (true) {
const action = yield take('*');
const state = yield select();
console.log('action', action);
console.log('state after', state);
}
}
複製代碼
代碼分析:
這是一個簡單的打印日誌功能 line 5: 指示 middleware 等待一個特定的 action。這裏整個Generator被暫停了,直到匹配到的action被dispatch了,這裏是*,因此是任意一個action; yield take('*')的返回值就是匹配到的action line 6: 用select api 拿到全部狀態 line 4-9: 這裏用了
while(true)
,由於 Generator 函數不具有 從運行至完成 的行爲(run-to-completion behavior),這個Generator 會每次迭代到第5行時阻塞,以等待 action 發起。
對比takeEvery
,實現同樣的效果:
import { select, takeEvery } from 'redux-saga/effects'
function* watchAndLog() {
yield takeEvery('*', function* logger(action) { // 這裏action被被動注入回調了
const state = yield select()
console.log('action', action)
console.log('state after', state)
})
}
複製代碼
能夠看出,takeEvery 的實現中, 匹配到action就執行回調, action就被動的被 push 到任務處理函數的。 每次 action 被匹配時任務處理函數就會一遍又一遍地被調用。而且它們也沒法控制什麼時候中止監聽。 而 take 的實現中,Saga 是本身主動 pull action 的,就像是在執行一個普通函數同樣:
action = getNextAction()
。主動拿到action就能夠控制中止,流程上更靈活;
eg: 監聽用戶的操做,並在用戶初次建立完三條 Todo 信息時顯示祝賀信息
import { take, put } from 'redux-saga/effects'
function* watchFirstThreeTodosCreation() {
for (let i = 0; i < 3; i++) {
const action = yield take('TODO_CREATED')
}
yield put({type: 'SHOW_CONGRATULATION'})
}
複製代碼
action被匹配到3次以後,Generator 會被回收而且相應的監聽不會再發生
主動拉取 action 可讓咱們使用熟悉的同步風格來描述咱們的控制流 eg: 監聽得來,還有順序
function* loginFlow() {
while (true) {
yield take('LOGIN')
// ... perform the login logic
yield take('LOGOUT')
// ... perform the logout logic
}
}
複製代碼
二、無阻塞調用 —— fork
就着上面說的登陸登出流程,先提早看一段代碼(有問題的):
import { take, call, put } from 'redux-saga/effects'
import Api from '...'
function* authorize(user, password) {
try {
const token = yield call(Api.authorize, user, password)
yield put({type: 'LOGIN_SUCCESS', token})
return token
} catch(error) {
yield put({type: 'LOGIN_ERROR', error})
}
}
function* loginFlow() {
while(true) {
const {user, password} = yield take('LOGIN_REQUEST')
const token = yield call(authorize, user, password)
if(token) {
yield call(Api.storeItem({token}))
yield take('LOGOUT')
yield call(Api.clearItem('token'))
}
}
}
複製代碼
line 16: 當 LOGIN_REQUEST 的action被匹配時,拿到用戶名密碼就去調用 authorize 這個Generator (PS: call 不只能夠用來調用返回 Promise 的函數。咱們也能夠用它來調用其餘 Generator 函數。 ) line 4-12: 拿到用戶名密碼以後就去執行真正的請求,這時候 authorize 就被阻塞了,等待着拿token;拿到 token 就 dispatch 登陸成功,返回token;登陸失敗就 dispatch 登陸失敗 line 18-22: 登陸成功以後就緩存token,而且監聽登出的action,當匹配LOGOUT,則清楚token
上面的代碼流程很清晰,就像閱讀同步代碼同樣,天然順序肯定了執行步驟,不用專門理解控制流(若是用takeEvery就會須要去理解)
可是,上面的代碼有問題。 當用戶點登陸以後,authorize 被阻塞,請求還沒返回,token還沒拿到,就在此刻,用戶又點了登出,那麼...
上面代碼的問題是 call 是一個會阻塞的 Effect。即 Generator 在調用結束以前不能執行或處理任何其餘事情,而後,LOGOUT 與調用 authorize 是 併發的,致使出問題了
因此,須要本小節的主角登場 —— ☆ fork ☆
fork 一個 任務,任務會在後臺啓動,Generator不會被阻塞,調用者能夠繼續它本身的流程,而不用等待被 fork 的任務結束。
具體改進以下:
import { fork, call, take, put, cancel } from 'redux-saga/effects'
import Api from '...'
function* authorize(user, password) {
try {
const token = yield call(Api.authorize, user, password)
yield put({type: 'LOGIN_SUCCESS', token})
yield call(Api.storeItem, {token})
} catch(error) {
yield put({type: 'LOGIN_ERROR', error})
} finally {
if (yield cancelled()) {
// 取消task以後的操做,好比取消loading之類
}
}
}
function* loginFlow() {
while (true) {
const {user, password} = yield take('LOGIN_REQUEST')
const task = yield fork(authorize, user, password)
const action = yield take(['LOGOUT', 'LOGIN_ERROR'])
if (action.type === 'LOGOUT') {
yield cancel(task)
}
yield call(Api.clearItem, 'token')
}
}
複製代碼
yield fork 的結果是一個Task Object. line 21: 改用 fork api 調用 authorize ,loginFlow 就不會被阻塞 line 22:監聽 2 個併發的 action, line 22-26: 會有三種狀況: 一、在登出以前,token已經拿到了,那麼會 dispatch LOGIN_SUCCESS,就結束了,就算在登出流程也是正常的 二、在登出以前,登陸失敗了,那麼會 dispatch LOGIN_ERROR ,而後清除token,結束;進入另一個 while 迭代等待下一個 LOGIN_REQUEST 三、token還沒拿到,用戶就登出了,那 loginFlow 會匹配到 LOGOUT ,取消掉 authorize 處理進程,清除token,而後就等待下一個 LOGIN_REQUEST 了 line 8: 使用 fork 以後就拿不到token了,由於不該該等待它,因此將 token 存儲操做移到 authorize 任務內部了 line 11-15:若是task被取消以後,你還須要作一些操做,好比Loading原本是true的,你想改爲false,那能夠利用
canceled
這個api來肯定是否取消了
三、在多個 Effects 之間啓動 race eg: 觸發一個遠程的獲取請求,而且限制了 1 秒內響應,不然做超時處理
import { race, call, put } from 'redux-saga/effects'
import { delay } from 'redux-saga'
function* fetchPostsWithTimeout() {
const {posts, timeout} = yield race({
posts: call(fetchApi, '/posts'),
timeout: call(delay, 1000)
})
if (posts) {
put({type: 'POSTS_RECEIVED', posts})
} else {
put({type: 'TIMEOUT_ERROR'})
}
}
複製代碼
四、經過yield*
進行排序
function* playLevelOne() { ... }
function* playLevelTwo() { ... }
function* playLevelThree() { ... }
function* game() {
// 利用 yield* 組織saga的順序
const score1 = yield* playLevelOne() // ※
yield put(showScore(score1))
const score2 = yield* playLevelTwo() // ※
yield put(showScore(score2))
const score3 = yield* playLevelThree() // ※
yield put(showScore(score3))
}
複製代碼
更多高級概念,可直達這裏學習
通常狀況下,action
都是符合 FSA 標準的(即:a plain javascript object),以下:
{
type: 'ADD_TODO',
payload: {
text: 'Do something.'
}
}
複製代碼
含義:當執行dispatch(action)
時,通知reducer
,而且把action.payload (新狀態數據)
以action.type
的方式(操做) 同步更新 到本地store。
可是,涉及請求的時候,payload通常來自於遠程服務端;而後redux-thunk就以 middleware 的形式來加強 redux store 的 dispatch 方法,(即支持 dispatch(function)
),看下面代碼:
// action.js
// -----------------
// 符合 FSA 的 action
export const setReplyModalData = (data) => {
return { type: SET_REPLY_MODAL_DATA, payload:{data} };
};
// 這個 action return 的是一個function
// function 中包含了業務數據請求代碼邏輯
export function fetchData(someValue) {
return (dispatch, getState) => {
myAjaxLib.post("/someEndpoint", { data: someValue })
.then(response => dispatch({ type: "REQUEST_SUCCEEDED", payload: response })
.catch(error => dispatch({ type: "REQUEST_FAILED", error: error });
};
}
// component.js
// ------------
// View 層 dispatch(fn) 觸發異步請求
// 這裏省略部分代碼
this.props.dispatch(fetchData({ hello: 'saga' }));
複製代碼
一樣的代碼,redux-saga的實現: 它單獨起一個新文件saga.js,而後把異步action遷移到裏面
// saga.js
// ----------
// worker saga
// 它是一個 generator function
// function 中也包含了業務數據請求代碼邏輯,但 是同步的寫法
function* fetchData(action) {
const { payload: { someValue } } = action;
try {
const result = yield call(myAjaxLib.post, "/someEndpoint", { data: someValue });
yield put({ type: "REQUEST_SUCCEEDED", payload: response });
} catch (error) {
yield put({ type: "REQUEST_FAILED", error: error });
}
}
// watcher saga
// 監聽每一次 dispatch(action)
// 若是 action.type === 'REQUEST',那麼執行 fetchData
export function* watchFetchData() {
yield takeEvery('REQUEST', fetchData);
}
// component.js
// -------
// View 層 dispatch(action) 觸發異步請求
// 這裏的 action 依然能夠是一個 plain object
this.props.dispatch({
type: 'REQUEST',
payload: {
someValue: { hello: 'saga' }
}
});
// action.js
// 而後action裏就保持了全部都是符合FSA的action了,更乾淨
export const setReplyModalData = (data) => {
return { type: SET_REPLY_MODAL_DATA, payload:{data} };
};
複製代碼
綜上,redux-saga對比redux-thunk的優勢:
反作用轉移到單獨的saga.js中,再也不摻雜在action.js中,保持 action 的簡單純粹,又使得異步操做集中能夠被集中處理。
dispatch 的參數依然是一個純粹的 action (FSA),而不是充滿 「黑魔法」 thunk function。
每個 saga 都是 一個 generator function,代碼採用 同步書寫 的方式來處理 異步邏輯(No Callback Hell),代碼變得更易讀
受益於 generator function 的 saga 實現,代碼異常/請求失敗 均可以直接經過 try/catch 語法直接捕獲處理。
請求都要加上try catch,多考慮,避免網頁掛掉; 那何時寫具體的catch呢?
感受 若是是要獲取數據的時候最好寫清楚catch,由於這種狀況下後端的toast通常都是網絡請求失敗這種mw,省得拿不到數據就啥也作不了了,這時,做爲前端,能夠給到用戶一個友好的toast。若是後端沒返回errmsg,頁面也沒任何提示就特別不友好若是是建立、編輯、新增等,就不須要前端去作toast,在接口統一處去toast後端返回的error message,才能夠toast具體的緣由,好比羣組重名了(這個是須要後端去查庫的)?仍是什麼數據不合法?仍是其餘緣由,就是提交類型的接口,在前端能作的表單校驗完成以後,仍是有接口報錯,那是後端才能檢查出來的,就toast後端的拋出來的問題;