redux-saga 初識(shí)和使用
redux-saga 是一個(gè)管理 Redux 應(yīng)用異步操作的中間件,功能類似redux-thunk + async/await, 它通過(guò)創(chuàng)建 Sagas 將所有的異步操作邏輯存放在一個(gè)地方進(jìn)行集中處理。
redux-saga 的 effects
redux-saga中的 Effects 是一個(gè)純文本 JavaScript 對(duì)象,包含一些將被 saga middleware 執(zhí)行的指令。這些指令所執(zhí)行的操作包括如下三種:
- 發(fā)起一個(gè)異步調(diào)用(如發(fā)一起一個(gè) Ajax 請(qǐng)求)
- 發(fā)起其他的 action 從而更新 Store
- 調(diào)用其他的 Sagas
Effects 中包含的指令有很多,具體可以異步API 參考進(jìn)行查閱
redux-saga 的特點(diǎn)
方便測(cè)試,例如:
assert.deepEqual(iterator.next().value, call(Api.fetch, '/products'))
- action 可以保持其純凈性,異步操作集中在 saga 中進(jìn)行處理
- watch/worker(監(jiān)聽(tīng)->執(zhí)行) 的工作形式
- 被實(shí)現(xiàn)為 generator
- 對(duì)含有復(fù)雜異步邏輯的應(yīng)用場(chǎng)景支持良好
- 更細(xì)粒度地實(shí)現(xiàn)異步邏輯,從而使流程更加清晰明了,遇到 bug 易于追蹤和解決。
- 以同步的方式書寫異步邏輯,更符合人的思維邏輯
- 從 redux-thunk 到 redux-saga
假如現(xiàn)在有一個(gè)場(chǎng)景:用戶在登錄的時(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 }); //阻塞,請(qǐng)求后臺(tái)數(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)解讀
對(duì)于 redux-saga, 還是有很多比較難以理解和晦澀的地方,下面筆者針對(duì)自己覺(jué)得比較容易混淆的概念進(jìn)行整理:
take 的使用
take 和 takeEvery 都是監(jiān)聽(tīng)某個(gè) action, 但是兩者的作用卻不一致,takeEvery 是每次 action 觸發(fā)的時(shí)候都響應(yīng),而 take 則是執(zhí)行流執(zhí)行到 take 語(yǔ)句時(shí)才響應(yīng)。takeEvery 只是監(jiān)聽(tīng) action, 并執(zhí)行相對(duì)應(yīng)的處理函數(shù),對(duì)何時(shí)執(zhí)行 action 以及如何響應(yīng) action 并沒(méi)有多大的控制權(quán),被調(diào)用的任務(wù)無(wú)法控制何時(shí)被調(diào)用,并且它們也無(wú)法控制何時(shí)停止監(jiān)聽(tīng),它只能在每次 action 被匹配時(shí)一遍又一遍地被調(diào)用。但是 take 可以在 generator 函數(shù)中決定何時(shí)響應(yīng)一個(gè) action 以及 響應(yīng)后的后續(xù)操作。
例如在監(jiān)聽(tīng)所有類型的 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),通過(guò)等待一個(gè)新的任意的 action 來(lái)啟動(dòng)一個(gè)新的迭代(logger 流程)。
阻塞和非阻塞
call 操作是用來(lái)發(fā)起異步操作的,對(duì)于 generator 來(lái)說(shuō),call 是阻塞的操作,它在 Generator 調(diào)用結(jié)束之前不能執(zhí)行或處理任何其他事情。,但是 fork 卻是非阻塞操作,當(dāng) fork 調(diào)動(dòng)任務(wù)時(shí),該任務(wù)會(huì)在后臺(tái)執(zhí)行,此時(shí)的執(zhí)行流可以繼續(xù)往后面執(zhí)行而不用等待結(jié)果返回。
例如如下的登錄場(chǎng)景:
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 在去請(qǐng)求 authorize 時(shí),結(jié)果未返回,但是此時(shí)用戶又觸發(fā)了 LOGOUT 的 action,此時(shí)的 LOGOUT 將會(huì)被忽略而不被處理,因?yàn)?loginFlow 在 authorize 中被堵塞了,沒(méi)有執(zhí)行到 take('LOGOUT')那里
同時(shí)執(zhí)行多個(gè)任務(wù)
如若遇到某個(gè)場(chǎng)景需要同一時(shí)間執(zhí)行多個(gè)任務(wù),比如 請(qǐng)求 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ī)則來(lái)執(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 對(duì)象,該 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í)行相對(duì)應(yīng)的函數(shù),每一個(gè) effect 函數(shù)都在 proc.js 實(shí)現(xiàn)了。
除了一些核心方法之外,redux-saga 還提供了一系列的 helper 文件,這些文件的作用是返回一個(gè)類 iterator 的對(duì)象,便于后續(xù)的遍歷和執(zhí)行, 在此不具體分析。
以上就是本文的全部?jī)?nèi)容,希望對(duì)大家的學(xué)習(xí)有所幫助,也希望大家多多支持腳本之家。
相關(guān)文章
webpack多頁(yè)面開(kāi)發(fā)實(shí)踐
這篇文章主要介紹了webpack多頁(yè)面開(kāi)發(fā)實(shí)踐,小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2017-12-12
原生JS實(shí)現(xiàn)圖片輪播與淡入效果的簡(jiǎn)單實(shí)例
下面小編就為大家?guī)?lái)一篇原生JS實(shí)現(xiàn)圖片輪播與淡入效果的簡(jiǎn)單實(shí)例。小編覺(jué)得挺不錯(cuò)的,現(xiàn)在就分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2016-08-08
JavaScript 數(shù)組運(yùn)用實(shí)現(xiàn)代碼
復(fù)習(xí)一下JS中數(shù)組的運(yùn)用。學(xué)習(xí)js數(shù)組的朋友可以參考下。2010-04-04
關(guān)于IE只能嵌套27層表格的說(shuō)法證明
關(guān)于IE只能嵌套27層表格的說(shuō)法證明...2006-11-11
JavaScript常用基礎(chǔ)知識(shí)強(qiáng)化學(xué)習(xí)
這篇文章主要介紹了JavaScript常用基礎(chǔ)知識(shí)強(qiáng)化學(xué)習(xí),需要的朋友可以參考下2015-12-12
JS實(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-09
JS實(shí)現(xiàn)隨機(jī)生成字符串(可指定長(zhǎng)度)的示例代碼
本文主要介紹了JS實(shí)現(xiàn)隨機(jī)生成字符串(可指定長(zhǎng)度)的示例代碼,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2023-08-08

