redux-saga 初識和使用
redux-saga 是一個(gè)管理 Redux 應(yīng)用異步操作的中間件,功能類似redux-thunk + async/await, 它通過創(chuàng)建 Sagas 將所有的異步操作邏輯存放在一個(gè)地方進(jìn)行集中處理。
redux-saga 的 effects
redux-saga中的 Effects 是一個(gè)純文本 JavaScript 對象,包含一些將被 saga middleware 執(zhí)行的指令。這些指令所執(zhí)行的操作包括如下三種:
- 發(fā)起一個(gè)異步調(diào)用(如發(fā)一起一個(gè) Ajax 請求)
- 發(fā)起其他的 action 從而更新 Store
- 調(diào)用其他的 Sagas
Effects 中包含的指令有很多,具體可以異步API 參考進(jìn)行查閱
redux-saga 的特點(diǎn)
方便測試,例如:
assert.deepEqual(iterator.next().value, call(Api.fetch, '/products'))
- action 可以保持其純凈性,異步操作集中在 saga 中進(jìn)行處理
- watch/worker(監(jiān)聽->執(zhí)行) 的工作形式
- 被實(shí)現(xiàn)為 generator
- 對含有復(fù)雜異步邏輯的應(yīng)用場景支持良好
- 更細(xì)粒度地實(shí)現(xiàn)異步邏輯,從而使流程更加清晰明了,遇到 bug 易于追蹤和解決。
- 以同步的方式書寫異步邏輯,更符合人的思維邏輯
- 從 redux-thunk 到 redux-saga
假如現(xiàn)在有一個(gè)場景:用戶在登錄的時(shí)候需要驗(yàn)證用戶的 username 和 password 是否符合要求。
使用 redux-thunk 實(shí)現(xiàn)
獲取用戶數(shù)據(jù)的邏輯(user.js):
// 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 }); } }
驗(yàn)證登錄的邏輯(login.js):
import request from 'axios'; import { loadUserData } from './user'; 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 }); } }
redux-saga
異步邏輯可以全部寫進(jìn) saga.js 中:
export function* loginSaga() { while(true) { const { user, pass } = yield take(LOGIN_REQUEST) //等待 Store 上指定的 action LOGIN_REQUEST try { let { data } = yield call(loginRequest, { user, pass }); //阻塞,請求后臺數(shù)據(jù) yield fork(loadUserData, data.uid); //非阻塞執(zhí)行l(wèi)oadUserData yield put({ type: LOGIN_SUCCESS, data }); //發(fā)起一個(gè)action,類似于dispatch } catch(error) { yield put({ type: LOGIN_ERROR, error }); } } } export function* loadUserData(uid) { try { yield put({ type: USERDATA_REQUEST }); let { data } = yield call(userRequest, `/users/${uid}`); yield put({ type: USERDATA_SUCCESS, data }); } catch(error) { yield put({ type: USERDATA_ERROR, error }); } }
難點(diǎn)解讀
對于 redux-saga, 還是有很多比較難以理解和晦澀的地方,下面筆者針對自己覺得比較容易混淆的概念進(jìn)行整理:
take 的使用
take 和 takeEvery 都是監(jiān)聽某個(gè) action, 但是兩者的作用卻不一致,takeEvery 是每次 action 觸發(fā)的時(shí)候都響應(yīng),而 take 則是執(zhí)行流執(zhí)行到 take 語句時(shí)才響應(yīng)。takeEvery 只是監(jiān)聽 action, 并執(zhí)行相對應(yīng)的處理函數(shù),對何時(shí)執(zhí)行 action 以及如何響應(yīng) action 并沒有多大的控制權(quán),被調(diào)用的任務(wù)無法控制何時(shí)被調(diào)用,并且它們也無法控制何時(shí)停止監(jiān)聽,它只能在每次 action 被匹配時(shí)一遍又一遍地被調(diào)用。但是 take 可以在 generator 函數(shù)中決定何時(shí)響應(yīng)一個(gè) action 以及 響應(yīng)后的后續(xù)操作。
例如在監(jiān)聽所有類型的 action 觸發(fā)時(shí)進(jìn)行 logger 操作,使用 takeEvery 實(shí)現(xiàn)如下:
import { takeEvery } from 'redux-saga' function* watchAndLog(getState) { yield* takeEvery('*', function* logger(action) { //do some logger operation //在回調(diào)函數(shù)體內(nèi) }) }
使用 take 實(shí)現(xiàn)如下:
import { take } from 'redux-saga/effects' function* watchAndLog(getState) { while(true) { const action = yield take('*') //do some logger operation //與 take 并行 }) }
其中 while(true) 的意思是一旦到達(dá)流程最后一步(logger),通過等待一個(gè)新的任意的 action 來啟動(dòng)一個(gè)新的迭代(logger 流程)。
阻塞和非阻塞
call 操作是用來發(fā)起異步操作的,對于 generator 來說,call 是阻塞的操作,它在 Generator 調(diào)用結(jié)束之前不能執(zhí)行或處理任何其他事情。,但是 fork 卻是非阻塞操作,當(dāng) fork 調(diào)動(dòng)任務(wù)時(shí),該任務(wù)會(huì)在后臺執(zhí)行,此時(shí)的執(zhí)行流可以繼續(xù)往后面執(zhí)行而不用等待結(jié)果返回。
例如如下的登錄場景:
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')) } } }
若在 call 在去請求 authorize 時(shí),結(jié)果未返回,但是此時(shí)用戶又觸發(fā)了 LOGOUT 的 action,此時(shí)的 LOGOUT 將會(huì)被忽略而不被處理,因?yàn)?loginFlow 在 authorize 中被堵塞了,沒有執(zhí)行到 take('LOGOUT')那里
同時(shí)執(zhí)行多個(gè)任務(wù)
如若遇到某個(gè)場景需要同一時(shí)間執(zhí)行多個(gè)任務(wù),比如 請求 users 數(shù)據(jù) 和 products 數(shù)據(jù), 應(yīng)該使用如下的方式:
import { call } from 'redux-saga/effects' //同步執(zhí)行 const [users, products] = yield [ call(fetch, '/users'), call(fetch, '/products') ] //而不是 //順序執(zhí)行 const users = yield call(fetch, '/users'), products = yield call(fetch, '/products')
當(dāng) yield 后面是一個(gè)數(shù)組時(shí),那么數(shù)組里面的操作將按照 Promise.all 的執(zhí)行規(guī)則來執(zhí)行,genertor 會(huì)阻塞知道所有的 effects 被執(zhí)行完成
源碼解讀
在每一個(gè)使用 redux-saga 的項(xiàng)目中,主文件中都會(huì)有如下一段將 sagas 中間件加入到 Store 的邏輯:
const sagaMiddleware = createSagaMiddleware({sagaMonitor}) const store = createStore( reducer, applyMiddleware(sagaMiddleware) ) sagaMiddleware.run(rootSaga)
其中 createSagaMiddleware 是 redux-saga 核心源碼文件 src/middleware.js 中導(dǎo)出的方法:
export default function sagaMiddlewareFactory({ context = {}, ...options } = {}) { ... function sagaMiddleware({ getState, dispatch }) { const channel = stdChannel() channel.put = (options.emitter || identity)(channel.put) sagaMiddleware.run = runSaga.bind(null, { context, channel, dispatch, getState, sagaMonitor, logger, onError, effectMiddlewares, }) return next => action => { if (sagaMonitor && sagaMonitor.actionDispatched) { sagaMonitor.actionDispatched(action) } const result = next(action) // hit reducers channel.put(action) return result } } ... }
這段邏輯主要是執(zhí)行了 sagaMiddleware(),該函數(shù)里面將 runSaga 賦值給 sagaMiddleware.run 并執(zhí)行,最后返回 middleware。 接著看 runSaga() 的邏輯:
export function runSaga(options, saga, ...args) { ... const task = proc( iterator, channel, wrapSagaDispatch(dispatch), getState, context, { sagaMonitor, logger, onError, middleware }, effectId, saga.name, ) if (sagaMonitor) { sagaMonitor.effectResolved(effectId, task) } return task }
這個(gè)函數(shù)里定義了返回了一個(gè) task 對象,該 task 是由 proc 產(chǎn)生的,移步 proc.js:
export default function proc( iterator, stdChannel, dispatch = noop, getState = noop, parentContext = {}, options = {}, parentEffectId = 0, name = 'anonymous', cont, ) { ... const task = newTask(parentEffectId, name, iterator, cont) const mainTask = { name, cancel: cancelMain, isRunning: true } const taskQueue = forkQueue(name, mainTask, end) ... next() return task function next(arg, isErr){ ... if (!result.done) { digestEffect(result.value, parentEffectId, '', next) } ... } }
其中 digestEffect 就執(zhí)行了 effectTriggerd() 和 runEffect(),也就是執(zhí)行 effect,其中 runEffect() 中定義了不同 effect 執(zhí)行相對應(yīng)的函數(shù),每一個(gè) effect 函數(shù)都在 proc.js 實(shí)現(xiàn)了。
除了一些核心方法之外,redux-saga 還提供了一系列的 helper 文件,這些文件的作用是返回一個(gè)類 iterator 的對象,便于后續(xù)的遍歷和執(zhí)行, 在此不具體分析。
以上就是本文的全部內(nèi)容,希望對大家的學(xué)習(xí)有所幫助,也希望大家多多支持腳本之家。
相關(guān)文章
原生JS實(shí)現(xiàn)圖片輪播與淡入效果的簡單實(shí)例
下面小編就為大家?guī)硪黄鶭S實(shí)現(xiàn)圖片輪播與淡入效果的簡單實(shí)例。小編覺得挺不錯(cuò)的,現(xiàn)在就分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧2016-08-08JavaScript 數(shù)組運(yùn)用實(shí)現(xiàn)代碼
復(fù)習(xí)一下JS中數(shù)組的運(yùn)用。學(xué)習(xí)js數(shù)組的朋友可以參考下。2010-04-04JavaScript常用基礎(chǔ)知識強(qiáng)化學(xué)習(xí)
這篇文章主要介紹了JavaScript常用基礎(chǔ)知識強(qiáng)化學(xué)習(xí),需要的朋友可以參考下2015-12-12JS實(shí)現(xiàn)帶關(guān)閉功能的阿里媽媽網(wǎng)站頂部滑出banner工具條代碼
這篇文章主要介紹了JS實(shí)現(xiàn)帶關(guān)閉功能的阿里媽媽網(wǎng)站頂部滑出banner工具條代碼,可實(shí)現(xiàn)頂部banner窗口的浮動(dòng)顯示及關(guān)閉隱藏功能,具有一定參考借鑒價(jià)值,需要的朋友可以參考下2015-09-09JS實(shí)現(xiàn)隨機(jī)生成字符串(可指定長度)的示例代碼
本文主要介紹了JS實(shí)現(xiàn)隨機(jī)生成字符串(可指定長度)的示例代碼,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2023-08-08