redux-saga 初識和使用
redux-saga 是一個管理 Redux 應用異步操作的中間件,功能類似redux-thunk + async/await, 它通過創(chuàng)建 Sagas 將所有的異步操作邏輯存放在一個地方進行集中處理。
redux-saga 的 effects
redux-saga中的 Effects 是一個純文本 JavaScript 對象,包含一些將被 saga middleware 執(zhí)行的指令。這些指令所執(zhí)行的操作包括如下三種:
- 發(fā)起一個異步調(diào)用(如發(fā)一起一個 Ajax 請求)
- 發(fā)起其他的 action 從而更新 Store
- 調(diào)用其他的 Sagas
Effects 中包含的指令有很多,具體可以異步API 參考進行查閱
redux-saga 的特點
方便測試,例如:
assert.deepEqual(iterator.next().value, call(Api.fetch, '/products'))
- action 可以保持其純凈性,異步操作集中在 saga 中進行處理
- watch/worker(監(jiān)聽->執(zhí)行) 的工作形式
- 被實現(xiàn)為 generator
- 對含有復雜異步邏輯的應用場景支持良好
- 更細粒度地實現(xiàn)異步邏輯,從而使流程更加清晰明了,遇到 bug 易于追蹤和解決。
- 以同步的方式書寫異步邏輯,更符合人的思維邏輯
- 從 redux-thunk 到 redux-saga
假如現(xiàn)在有一個場景:用戶在登錄的時候需要驗證用戶的 username 和 password 是否符合要求。
使用 redux-thunk 實現(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 });
}
}
驗證登錄的邏輯(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
異步邏輯可以全部寫進 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ā)起一個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 });
}
}
難點解讀
對于 redux-saga, 還是有很多比較難以理解和晦澀的地方,下面筆者針對自己覺得比較容易混淆的概念進行整理:
take 的使用
take 和 takeEvery 都是監(jiān)聽某個 action, 但是兩者的作用卻不一致,takeEvery 是每次 action 觸發(fā)的時候都響應,而 take 則是執(zhí)行流執(zhí)行到 take 語句時才響應。takeEvery 只是監(jiān)聽 action, 并執(zhí)行相對應的處理函數(shù),對何時執(zhí)行 action 以及如何響應 action 并沒有多大的控制權(quán),被調(diào)用的任務無法控制何時被調(diào)用,并且它們也無法控制何時停止監(jiān)聽,它只能在每次 action 被匹配時一遍又一遍地被調(diào)用。但是 take 可以在 generator 函數(shù)中決定何時響應一個 action 以及 響應后的后續(xù)操作。
例如在監(jiān)聽所有類型的 action 觸發(fā)時進行 logger 操作,使用 takeEvery 實現(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 實現(xiàn)如下:
import { take } from 'redux-saga/effects'
function* watchAndLog(getState) {
while(true) {
const action = yield take('*')
//do some logger operation //與 take 并行
})
}
其中 while(true) 的意思是一旦到達流程最后一步(logger),通過等待一個新的任意的 action 來啟動一個新的迭代(logger 流程)。
阻塞和非阻塞
call 操作是用來發(fā)起異步操作的,對于 generator 來說,call 是阻塞的操作,它在 Generator 調(diào)用結(jié)束之前不能執(zhí)行或處理任何其他事情。,但是 fork 卻是非阻塞操作,當 fork 調(diào)動任務時,該任務會在后臺執(zhí)行,此時的執(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 時,結(jié)果未返回,但是此時用戶又觸發(fā)了 LOGOUT 的 action,此時的 LOGOUT 將會被忽略而不被處理,因為 loginFlow 在 authorize 中被堵塞了,沒有執(zhí)行到 take('LOGOUT')那里
同時執(zhí)行多個任務
如若遇到某個場景需要同一時間執(zhí)行多個任務,比如 請求 users 數(shù)據(jù) 和 products 數(shù)據(jù), 應該使用如下的方式:
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')
當 yield 后面是一個數(shù)組時,那么數(shù)組里面的操作將按照 Promise.all 的執(zhí)行規(guī)則來執(zhí)行,genertor 會阻塞知道所有的 effects 被執(zhí)行完成
源碼解讀
在每一個使用 redux-saga 的項目中,主文件中都會有如下一段將 sagas 中間件加入到 Store 的邏輯:
const sagaMiddleware = createSagaMiddleware({sagaMonitor})
const store = createStore(
reducer,
applyMiddleware(sagaMiddleware)
)
sagaMiddleware.run(rootSaga)
其中 createSagaMiddleware 是 redux-saga 核心源碼文件 src/middleware.js 中導出的方法:
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
}
這個函數(shù)里定義了返回了一個 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í)行相對應的函數(shù),每一個 effect 函數(shù)都在 proc.js 實現(xiàn)了。
除了一些核心方法之外,redux-saga 還提供了一系列的 helper 文件,這些文件的作用是返回一個類 iterator 的對象,便于后續(xù)的遍歷和執(zhí)行, 在此不具體分析。
以上就是本文的全部內(nèi)容,希望對大家的學習有所幫助,也希望大家多多支持腳本之家。
相關(guān)文章
BootStrap 動態(tài)添加驗證項和取消驗證項的實現(xiàn)方法
這篇文章主要介紹了BootStrap 動態(tài)添加驗證項和取消驗證項的實現(xiàn)方法的相關(guān)資料,非常不錯,具有參考借鑒價值,需要的朋友可以參考下2016-09-09
JS遍歷ul下的li點擊彈出li的索引的實現(xiàn)方法
這篇文章主要介紹了JS遍歷ul下的li點擊彈出li的索引的方法,非常不錯,具有參考借鑒價值,需要的朋友可以參考下2016-09-09
前端實現(xiàn)文件下載的4種常見方式與實戰(zhàn)示例
在前端開發(fā)中,實現(xiàn)文件下載是一個非常常見的需求,本篇文章將介紹 4 種前端文件下載的方式,并提供詳細示例,希望能幫助大家掌握文件下載的各種技巧2025-04-04
利用JavaScript做數(shù)獨的完整實現(xiàn)過程
數(shù)獨游戲是在一個9*9的方格中進行填數(shù)字的游戲,需要滿足的規(guī)則是每行每列和每個子九宮格都是1~9的不重復數(shù)字,下面這篇文章主要給大家介紹了關(guān)于如何利用JavaScript做數(shù)獨的相關(guān)資料,需要的朋友可以參考下2021-09-09

