文章同步於Github Pines-Cheng/blog
本質都是爲了解決異步action的問題
Redux Saga能夠理解爲一個和系統交互的常駐進程,其中,Saga可簡單定義以下:javascript
Saga = Worker + Watcher
saga特色:html
LLT(long live transcation)
等業務場景。takeLatest/takeEvery/throttle
方法,能夠便利的實現對事件的僅關注最近事件、關注每一次、事件限頻cancel/delay
方法,能夠便利的取消、延遲異步請求race(effects),[…effects]
方法來支持競態和並行場景Redux Saga適用於對事件操做有細粒度需求的場景,同時他們也提供了更好的可測試性。java
這裏有一個簡單的需求,登陸頁面,使用redux-thunk
與async / await
。組件可能看起來像這樣,像日常同樣分派操做。react
組件部分兩者應該是大同小異:ios
import { login } from 'redux/auth'; class LoginForm extends Component { onClick(e) { e.preventDefault(); const { user, pass } = this.refs; this.props.dispatch(login(user.value, pass.value)); } render() { return (<div> <input type="text" ref="user" /> <input type="password" ref="pass" /> <button onClick={::this.onClick}>Sign In</button> </div>); } } export default connect((state) => ({}))(LoginForm);
登陸的action文件git
// auth.js import request from 'axios'; import { loadUserData } from './user'; // define constants // define initial state // export default reducer export const login = (user, pass) => async (dispatch) => { try { dispatch({ type: LOGIN_REQUEST }); let { data } = await request.post('/login', { user, pass }); await dispatch(loadUserData(data.uid)); dispatch({ type: LOGIN_SUCCESS, data }); } catch(error) { dispatch({ type: LOGIN_ERROR, error }); } } // more actions...
更新用戶數據的頁面:github
// user.js import request from 'axios'; // define constants // define initial state // export default reducer 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 }); } } // more actions...
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 }); } }
咱們使用形式yield call(func,… args)
調用api函數。調用不會執行效果,它只是建立一個簡單的對象,如{type:’CALL’,func,args}
。執行被委託給redux-saga中間件,該中間件負責執行函數而且用其結果恢復generatorr。ajax
相比Redux Thunk,使用Redux Saga有幾處明顯的變化:redux
dispatch(action creator)
,而是dispatch(pure action)
ES6 Generator
語法,簡化異步代碼語法除開上述這些不一樣點,Redux Saga真正的威力,在於其提供了一系列幫助方法,使得對於各種事件能夠進行更細粒度的控制,從而完成更加複雜的操做。axios
const iterator = loginSaga() assert.deepEqual(iterator.next().value, take(LOGIN_REQUEST)) // resume the generator with some dummy action const mockAction = {user: '...', pass: '...'} assert.deepEqual( iterator.next(mockAction).value, call(request.post, '/login', mockAction) ) // simulate an error result const mockError = 'invalid user/password' assert.deepEqual( iterator.throw(mockError).value, put({ type: LOGIN_ERROR, error: mockError }) )
注意,咱們經過簡單地將模擬數據注入迭代器的下一個方法來檢查api調用結果。模擬數據比模擬函數更簡單。
經過yield take(ACTION)
能夠方便自由的對action進行攔截和過濾。Thunks由每一個新動做的動做建立者調用(例如LOGIN_REQUEST)。即動做被不斷地推送到thunk,而且thunk不能控制什麼時候中止處理那些動做。
假設例如咱們要添加如下要求:
你將如何實現這一點與thunk?同時還爲整個流程提供全面的測試覆蓋?
但是若是你使用redux-saga:
function* authorize(credentials) { const token = yield call(api.authorize, credentials) yield put( login.success(token) ) return token } function* authAndRefreshTokenOnExpiry(name, password) { let token = yield call(authorize, {name, password}) while(true) { yield call(delay, token.expires_in) token = yield call(authorize, {token}) } } function* watchAuth() { while(true) { try { const {name, password} = yield take(LOGIN_REQUEST) yield race([ take(LOGOUT), call(authAndRefreshTokenOnExpiry, name, password) ]) // user logged out, next while iteration will wait for the // next LOGIN_REQUEST action } catch(error) { yield put( login.error(error) ) } } }
在上面的例子中,咱們使用race表示了併發要求。
authAndRefreshTokenOnExpiry
後臺任務。authAndRefreshTokenOnExpiry
在調用(受權,{token})調用的中間被阻止,它也將被取消。取消自動向下傳播。有時候咱們須要在幾個ajax請求執行完以後,再執行對應的操做。redux-thunk須要藉助第三方的庫,而redux-saga是直接實現的。
import { call } from 'redux-saga/effects' // 正確寫法, effects 將會同步執行 const [users, repos] = yield [ call(fetch, '/users'), call(fetch, '/repos') ]
當咱們須要 yield 一個包含 effects 的數組, generator 會被阻塞直到全部的 effects 都執行完畢,或者當一個 effect 被拒絕 (就像 Promise.all 的行爲)。
在redux-saga中,咱們可使用了輔助函數 takeEvery 在每一個 action 來到時派生一個新的任務。 這多少有些模仿 redux-thunk 的行爲:舉個例子,每次一個組件調用 fetchProducts Action
建立器(Action Creator),Action 建立器就會發起一個 thunk 來執行控制流。
在現實狀況中,takeEvery 只是一個在強大的低階 API 之上構建的輔助函數。 在這一節中咱們將看到一個新的 Effect,即 take。take 讓咱們經過全面控制 action 觀察進程來構建複雜的控制流成爲可能。
讓咱們開始一個簡單的 Saga 例子,這個 Saga 將監聽全部發起到 store 的 action,而後將它們記錄到控制檯。
使用 takeEvery('')( 表明通配符模式),咱們就能捕獲發起的全部類型的 action。
import { takeEvery } from 'redux-saga' function* watchAndLog(getState) { yield* takeEvery('*', function* logger(action) { console.log('action', action) console.log('state after', getState()) }) }
如今咱們知道如何使用 take Effect 來實現和上面相同的功能:
import { take } from 'redux-saga/effects' function* watchAndLog(getState) { while(true) { const action = yield take('*') console.log('action', action) console.log('state after', getState()) }) }
take 就像咱們更早以前看到的 call 和 put。它建立另外一個命令對象,告訴 middleware 等待一個特定的 action。 正如在 call Effect 的狀況中,middleware 會暫停 Generator,直到返回的 Promise 被 resolve。 在 take 的狀況中,它將會暫停 Generator 直到一個匹配的 action 被髮起了。 在以上的例子中,watchAndLog 處於暫停狀態,直到任意的一個 action 被髮起。
注意,咱們運行了一個無限循環的 while(true)。記住這是一個 Generator 函數,它不具有 從運行至完成 的行爲(run-to-completion behavior)
。 Generator 將在每次迭代上阻塞以等待 action 發起。
一個簡單的例子,假設在咱們的 Todo 應用中,咱們但願監聽用戶的操做,並在用戶初次建立完三條 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'}) }
與 while(true) 不一樣,咱們運行一個只迭代三次的 for 循環。在 take 初次的 3 個 TODO_CREATED action 以後, watchFirstThreeTodosCreation Saga
將會使應用顯示一條祝賀信息而後停止。這意味着 Generator 會被回收而且相應的監聽不會再發生。
一旦任務被 fork,可使用 yield cancel(task)
來停止任務執行。取消正在運行的任務,將拋出 SagaCancellationException
錯誤。
爲了對 action 隊列進行防抖動,能夠在被 fork 的任務裏放置一個 delay。
const delay = (ms) => new Promise(resolve => setTimeout(resolve, ms)) function* handleInput(input) { // 500ms 防抖動 yield call(delay, 500) ... } function* watchInput() { let task while(true) { const { input } = yield take('INPUT_CHANGED') if(task) yield cancel(task) task = yield fork(handleInput, input) } }
在上面的示例中,handleInput 在執行以前等待了 500ms。若是用戶在此期間輸入了更多文字,咱們將收到更多的 INPUT_CHANGED action
。 而且因爲 handleInput 仍然會被 delay 阻塞,因此在執行本身的邏輯以前它會被 watchInput 取消。
一個 effect 就是一個純文本 JavaScript 對象,包含一些將被 saga middleware 執行的指令。
使用 redux-saga 提供的工廠函數來建立 effect。 舉個例子,你可使用 call(myfunc, 'arg1', 'arg2')
指示 middleware 調用 myfunc('arg1', 'arg2')
並將結果返回給 yield 了 effect 的那個 Generator。
從 Saga 內觸發異步操做(Side Effect)老是由 yield 一些聲明式的 Effect 來完成的 (你也能夠直接 yield Promise,可是這會讓測試變得困難。使用 Effect 諸如 call 和 put,與高階 API 如 takeEvery 相結合,讓咱們實現與 redux-thunk 一樣的東西, 但又有額外的易於測試的好處。
一個 task 就像是一個在後臺運行的進程。在基於 redux-saga 的應用程序中,能夠同時運行多個 task。經過 fork 函數來建立 task:
function* saga() { ... const task = yield fork(otherSaga, ...args) ... }
指的是一種使用兩個單獨的 Saga 來組織控制流的方式。
Watcher: 監聽發起的 action 並在每次接收到 action 時 fork 一個 worker。
Worker: 處理 action 並結束它。
示例:
function* watcher() { while(true) { const action = yield take(ACTION) yield fork(worker, action.payload) } } function* worker(payload) { // ... do some stuff }
建立一條 Effect 描述信息,指示 middleware 等待 Store 上指定的 action。 Generator 會暫停,直到一個與 pattern 匹配的 action 被髮起。
用如下規則來解釋 pattern:
take(action => action.entities
) 會匹配那些 entities 字段爲真的 action)。action.type === pattern
時被匹配(例如,take(INCREMENT_ASYNC))。take([INCREMENT, DECREMENT])
會匹配 INCREMENT 或 DECREMENT 類型的 action)。用於觸發 action,功能上相似於dispatch。
建立一條dispatch Effect
描述信息,指示 middleware 發起一個 action 到 Store。
直接使用dispatch:
//... function* fetchProducts(dispatch) const products = yield call(Api.fetch, '/products') dispatch({ type: 'PRODUCTS_RECEIVED', products }) }
該解決方案與咱們在上一節中看到的從 Generator 內部直接調用函數,有着相同的缺點。若是咱們想要測試 fetchProducts 接收到 AJAX 響應以後執行 dispatch, 咱們還須要模擬 dispatch 函數。
相反,咱們須要一樣的聲明式的解決方案。只需建立一個對象來指示 middleware 咱們須要發起一些 action,而後讓 middleware 執行真實的 dispatch。 這種方式咱們就能夠一樣的方式測試 Generator 的 dispatch:只需檢查 yield 後的 Effect,並確保它包含正確的指令。
redux-saga 爲此提供了另一個函數 put,這個函數用於建立 dispatch Effect
。
import { call, put } from 'redux-saga/effects' //... function* fetchProducts() { const products = yield call(Api.fetch, '/products') // 建立並 yield 一個 dispatch Effect yield put({ type: 'PRODUCTS_RECEIVED', products }) }
如今,咱們能夠像上一節那樣輕易地測試 Generator:
import { call, put } from 'redux-saga/effects' import Api from '...' const iterator = fetchProducts() // 指望一個 call 指令 assert.deepEqual( iterator.next().value, call(Api.fetch, '/products'), "fetchProducts should yield an Effect call(Api.fetch, './products')" ) // 建立一個假的響應對象 const products = {} // 指望一個 dispatch 指令 assert.deepEqual( iterator.next(products).value, put({ type: 'PRODUCTS_RECEIVED', products }), "fetchProducts should yield an Effect put({ type: 'PRODUCTS_RECEIVED', products })" )
用於調用異步邏輯,支持 promise 。
建立一條 Effect 描述信息,指示 middleware 調用 fn 函數並以 args 爲參數。fn 既能夠是一個普通函數,也能夠是一個 Generator 函數。
middleware 調用這個函數並檢查它的結果。
若是結果是一個 Generator 對象,middleware 會執行它,就像在啓動 Generator (startup Generators
,啓動時被傳給 middleware)時作的。 若是有子級 Generator,那麼在子級 Generator 正常結束前,父級 Generator 會暫停,這種狀況下,父級 Generator 將會在子級 Generator 返回後繼續執行,或者直到子級 Generator 被某些錯誤停止, 若是是這種狀況,將在父級 Generator 中拋出一個錯誤。
若是結果是一個 Promise,middleware 會暫停直到這個 Promise 被 resolve,resolve 後 Generator 會繼續執行。 或者直到 Promise 被 reject 了,若是是這種狀況,將在 Generator 中拋出一個錯誤。
當 Generator 中拋出了一個錯誤,若是有一個 try/catch 包裹當前的 yield 指令,控制權將被轉交給 catch。 不然,Generator 會被錯誤停止,而且若是這個 Generator 被其餘 Generator 調用了,錯誤將會傳到調用的 Generator。
yield fork(fn ...args) 的結果是一個 Task 對象 —— 一個具有某些有用的方法和屬性的對象
建立一條 Effect 描述信息,指示 middleware 以 無阻塞調用 方式執行 fn。
fork 相似於 call,能夠用來調用普通函數和 Generator 函數。但 fork 的調用是無阻塞的,在等待 fn 返回結果時,middleware 不會暫停 Generator。 相反,一旦 fn 被調用,Generator 當即恢復執行。
fork 與 race 相似,是一箇中心化的 Effect,管理 Sagas 間的併發。
建立一條 Effect 描述信息,指示 middleware 在多個 Effect 之間執行一個 race(相似 Promise.race([...]) 的行爲)。
redux-saga的其餘詳細API列舉以下,API詳解能夠查看API 參考
Middleware API
Saga Helpers
Effect creators
Effect combinators
Interfaces
External API