Redux-saga
概述
在 redux
一文中我們有說過處理異步我們應(yīng)該放在 reducer
之前,所以我們需要中間件來處理我們的異步操作拂酣。redux-saga
就是一個(gè)用于管理 redux
應(yīng)用的異步操作中間件,redux-saga
通過創(chuàng)建 sagas
將所有異步操作的邏輯都收集在一個(gè)位置集中處理痛侍。
簡(jiǎn)單的來說就是兩步:
-
sagas
負(fù)責(zé)協(xié)調(diào)那些復(fù)雜或者異步的操作 -
reducer
負(fù)責(zé)處理action
的stage
更新
數(shù)據(jù)流
我們先看一張圖來大致了解下整個(gè)過程:
有了之前 redux
的基礎(chǔ)惜颇,這張圖其實(shí)還是比較容易理解的,下面我們來簡(jiǎn)單說下:
-
React
組件由用戶觸發(fā)事件壶运,通過action creator
發(fā)起action
-
sagas
監(jiān)聽發(fā)起的action
,然后決定基于這個(gè)action
做什么 - 緊接著根據(jù)上步所做處理將
effects
(sage的任務(wù)單元浪秘,簡(jiǎn)單的JavaScript
對(duì)象)傳給reducer
- 最后由
reducer
返回新的state
安裝
yarn add redux-saga
// npm install redux-saga -S
使用實(shí)例
配置
import { createStore, combineReducers, applyMiddleware } from 'redux';
import global from './reducers/global';
import login from './reducers/login';
import createSagaMiddleware from 'redux-saga'; // 引入redux-saga中的createSagaMiddleware函數(shù)
import rootSaga from '../sagas'; // 引入saga.js
const sagaMiddleware = createSagaMiddleware() // 執(zhí)行
const reducerAll = {
global,
login
}
export const store = createStore(
combineReducers({...reducerAll}), // 合并reducer
applyMiddleware(sagaMiddleware) // 中間件蒋情,加載sagaMiddleware
)
sagaMiddleware.run(rootSaga)
ui組件觸發(fā)action創(chuàng)建函數(shù)
handleSubmit = (e) => {
this.props.form.validateFields((err, values) => {
if (!err) {
console.log('Received values of form: ', values);
this.props.onSubmit(values);
}
});
}
<Form onSubmit={this.handleSubmit} className="login-form">
...
</Form>
將action傳入saga
const mapDispatchToProps = dispatch => {
return {
onSubmit: (payload) => {
dispatch({ type: LOGIN_REQUEST, ...payload })
}
}
}
saga捕獲action創(chuàng)建函數(shù)返回的action(effects)
function* fetchLogin(userName, password) {
try {
console.log("saga:");
const token = yield call(axios.get({ url: '' }))
yield put({ type: 'LOGIN_SUCCESS', token })
} catch(error) {
yield put({ type: 'LOGIN_ERROR', error })
}
}
export default function* watchIsLogin(dispatch) {
while(true) {
const { userName, password } = yield take('LOGIN_REQUEST');
yield fork(fetchLogin, userName, password)
}
}
reducer接受并返回新的state
const initState = {
userName: 'Cola'
}
export default function(state = initState, action){
switch (action.type) {
case LOGIN_IN:
return {...state, ...action.payload};
default:
return state;
}
}
看了上面的步驟是不是覺得整體邏輯還是非常清晰明了的,下面我們把上面代碼中未講解到的代碼名詞解釋下耸携。
名詞解釋
Effect
一個(gè) effect
就是一個(gè)純文本 javascript
對(duì)象棵癣,包含一些將被 saga middleware
執(zhí)行的指令。那么如何創(chuàng)建 effect
呢夺衍?使用下面我們馬上要講到API中 redux-saga
提供的工廠函數(shù)來創(chuàng)建 effect
Task
一個(gè) task
就像是一個(gè)在后臺(tái)運(yùn)行的進(jìn)程狈谊。在基于 redux-saga
的應(yīng)用程序中,可以同時(shí)運(yùn)行多個(gè) task
沟沙。通過 fork
函數(shù)來創(chuàng)建 task
:
阻塞調(diào)用/非阻塞調(diào)用
阻塞調(diào)用的意思是河劝,Saga
在 yield Effect
之后會(huì)等待其執(zhí)行結(jié)果返回,結(jié)果返回后才會(huì)恢復(fù)執(zhí)行 Generator
中的下一個(gè)指令矛紫。
非阻塞調(diào)用的意思是赎瞎,Saga
會(huì)在 yield Effect
之后立即恢復(fù)執(zhí)行。
function* saga() {
yield take(ACTION) // 阻塞: 將等待 action
yield call(ApiFn, ...args) // 阻塞: 將等待 ApiFn (如果 ApiFn 返回一個(gè) Promise 的話)
yield call(otherSaga, ...args) // 阻塞: 將等待 otherSaga 結(jié)束
yield put(ACTION) // 非阻塞
yield put(channel, ACTION) // 當(dāng) put 沒有被緩存而是被 taker 立即消費(fèi)掉時(shí)颊咬,阻塞
const task = yield fork(otherSaga, ...args) // 非阻塞: 將不會(huì)等待 otherSaga
yield cancel(task) // 非阻塞: 將立即恢復(fù)執(zhí)行
// or
yield join(task) // 阻塞: 將等待 task 結(jié)束
}
Watcher/Worker
指的是一種使用兩個(gè)單獨(dú)的 Saga
來組織控制流的方式务甥。
- Watcher: 監(jiān)聽發(fā)起的
action
并在每次接收到action
時(shí)fork
一個(gè)worker
。 - Worker: 處理
action
并結(jié)束它喳篇。
function* watcher() {
while(true) {
const action = yield take(ACTION)
yield fork(worker, action.payload)
}
}
function* worker(payload) {
// ... do some stuff
}
Middleware API
createSagaMiddleware(options)
創(chuàng)建一個(gè) Redux middleware
敞临,并將 Sagas
連接到 Redux Store
。
options
: Object
- 傳遞給 middleware
的選項(xiàng)列表麸澜。
middleware.run(saga, ...args)
動(dòng)態(tài)執(zhí)行 saga
哟绊。用于 applyMiddleware
階段之后執(zhí)行 Sagas
。這個(gè)方法返回一個(gè)
Task
描述對(duì)象痰憎。
Saga輔助函數(shù)
takeEvery
在發(fā)起(dispatch
)到 Store
并且匹配 pattern
的每一個(gè) action
上派生一個(gè) saga
。簡(jiǎn)單來說就是監(jiān)聽所有的匹配到的 action
攀涵。
import { takeEvery } from `redux-saga/effects`
function* fetchUser(action) {
...
}
function* watchFetchUser() {
yield takeEvery('USER_REQUESTED', fetchUser)
}
takeLatest
在發(fā)起到 Store
并且匹配 pattern
的每一個(gè) action
上派生一個(gè) saga
铣耘。并自動(dòng)取消之前所有已經(jīng)啟動(dòng)但仍在執(zhí)行中的 saga
任務(wù)。
import { takeLatest } from `redux-saga/effects`
function* fetchUser(action) {
...
}
function* watchLastFetchUser() {
yield takeLatest('USER_REQUESTED', fetchUser)
}
這樣便可以保證:即使用戶以極快的速度連續(xù)多次觸發(fā) USER_REQUESTED
action
以故,我們都只會(huì)以最后的一個(gè)結(jié)束蜗细。
takeLeading
在發(fā)起到 Store
并且匹配 pattern
的每一個(gè) action
上派生一個(gè) saga
。 它將在派生一次任務(wù)之后阻塞,直到派生的 saga
完成炉媒,然后又再次開始監(jiān)聽指定的 pattern
踪区。
import { takeLeading } from `redux-saga/effects`
function* fetchUser(action) {
...
}
function* watchLastFetchUser() {
yield takeLeading('USER_REQUESTED', fetchUser)
}
由于 takeLeading
在其開始之后便無視所有新傳入的任務(wù),我們便可以保證:如果用戶以極快的速度連續(xù)多次觸發(fā) USER_REQUESTED action
吊骤,我們都只會(huì)保持以第一個(gè) action
運(yùn)行缎岗。
throttle
它在派生一次任務(wù)之后,仍然將新傳入的 action
接收到底層的 buffer
中白粉,至多保留(最近的)一個(gè)传泊。但與此同時(shí),它在 ms
毫秒內(nèi)將暫停派生新的任務(wù) —— 這也就是它被命名為節(jié)流閥(throttle
)的原因鸭巴。其用途眷细,是在處理任務(wù)時(shí),無視給定的時(shí)長(zhǎng)內(nèi)新傳入的 action
鹃祖。
import { call, put, throttle } from `redux-saga/effects`
function* fetchAutocomplete(action) {
const autocompleteProposals = yield call(Api.fetchAutocomplete, action.text)
yield put({type: 'FETCHED_AUTOCOMPLETE_PROPOSALS', proposals: autocompleteProposals})
}
function* throttleAutocomplete() {
yield throttle(1000, 'FETCH_AUTOCOMPLETE', fetchAutocomplete)
}
上面的代碼我們通過 throttle
無視了一段時(shí)間內(nèi)連續(xù)的 FETCH_AUTOCOMPLETE
溪椎,我們便可以確保用戶不會(huì)因此向我們的服務(wù)器發(fā)起大量請(qǐng)求。
Effect 創(chuàng)建器
注意:
- 以下每個(gè)函數(shù)都會(huì)返回一個(gè)普通 Javascript 對(duì)象(plain JavaScript > > object)恬口,并且不會(huì)執(zhí)行任何其它操作校读。
- 執(zhí)行是由 middleware 在上述迭代過程中進(jìn)行的。
- middleware 會(huì)檢查每個(gè) Effect 的描述信息楷兽,并進(jìn)行相應(yīng)的操作
take
創(chuàng)建一個(gè) Effect
描述信息地熄,用來命令 middleware
在 Store
上等待指定的 action
。 在發(fā)起匹配的 action
之前芯杀,Generator
將暫停端考。
put
創(chuàng)建一個(gè) Effect
描述信息,用來命令 middleware
向 Store
發(fā)起一個(gè) action
揭厚。 這個(gè) effect
是非阻塞型的却特,并且所有向下游拋出的錯(cuò)誤(例如在 reducer
中),都不會(huì)冒泡回到 saga
當(dāng)中筛圆。
call(fn, ...args)
創(chuàng)建一個(gè) Effect
描述信息裂明,用來命令 middleware
以參數(shù) args
調(diào)用函數(shù) fn
。
fork(fn, ...args)
創(chuàng)建一個(gè) Effect
描述信息太援,用來命令 middleware
以 非阻塞調(diào)用 的形式執(zhí)行 fn
闽晦。
join(task)
創(chuàng)建一個(gè) Effect
描述信息,用來命令 middleware
等待之前的一個(gè)分叉任務(wù)的結(jié)果提岔。
cancel(task)
創(chuàng)建一個(gè) Effect
描述信息仙蛉,用來命令 middleware
取消之前的一個(gè)分叉任務(wù)。
select(selector, ...args)
創(chuàng)建一個(gè) Effect
碱蒙,用來命令 middleware
在當(dāng)前 Store
的 state
上調(diào)用指定的選擇器(即返回 selector(getState(), ...args)
的結(jié)果)
例如荠瘪,假設(shè)我們?cè)趹?yīng)用程序中有這樣結(jié)構(gòu)的一份 state
:
state = {
cart: {...}
}
我們創(chuàng)建一個(gè) 選擇器(selector)夯巷,即一個(gè)知道如果從 State
中提取 cart
數(shù)據(jù)的函數(shù):
// selectors.js
export const getCart = state => state.cart
然后,我們可以使用 select Effect
從 Saga
的內(nèi)部使用該選擇器:
import { take, fork, select } from 'redux-saga/effects'
import { getCart } from './selectors'
function* checkout() {
// 使用被導(dǎo)出的選擇器查詢 state
const cart = yield select(getCart)
// ... 調(diào)用某些 API哀墓,然后發(fā)起一個(gè) success/error action
}
export default function* rootSaga() {
while (true) {
yield take('CHECKOUT_REQUEST')
yield fork(checkout)
}
}
actionChannel(pattern, [buffer])
創(chuàng)建一個(gè) Effect
趁餐,用來命令 middleware
通過一個(gè)事件 channel
對(duì)匹配 pattern
的 action
進(jìn)行排序。 作為可選項(xiàng)篮绰,你也可以提供一個(gè) buffer
來控制如何緩存排序的 actions
后雷。
import { actionChannel, call } from 'redux-saga/effects'
import api from '...'
function* takeOneAtMost() {
const chan = yield actionChannel('USER_REQUEST')
while (true) {
const {payload} = yield take(chan)
yield call(api.getUser, payload)
}
}
flush(channel)
創(chuàng)建一個(gè) Effect
,用來命令 middleware
從 channel
中沖除所有被緩存的數(shù)據(jù)阶牍。被沖除的數(shù)據(jù)會(huì)返回至 saga
喷面,這樣便可以在需要的時(shí)候再次被利用。
function* saga() {
const chan = yield actionChannel('ACTION')
try {
while (true) {
const action = yield take(chan)
// ...
}
} finally {
const actions = yield flush(chan)
// ...
}
}
cancelled()
創(chuàng)建一個(gè) Effect
走孽,用來命令 middleware
返回該 generator
是否已經(jīng)被取消惧辈。通常你會(huì)在 finally
區(qū)塊中使用這個(gè) Effect
來運(yùn)行取消時(shí)專用的代碼。
function* saga() {
try {
// ...
} finally {
if (yield cancelled()) {
// 只應(yīng)在取消時(shí)執(zhí)行的邏輯
}
// 應(yīng)在所有情況下都執(zhí)行的邏輯(例如關(guān)閉一個(gè) channel)
}
}
setContext(props)
創(chuàng)建一個(gè) effect
磕瓷,用來命令 middleware
更新其自身的上下文盒齿。這個(gè) effect
擴(kuò)展了 saga
的上下文,而不是代替困食。
getContext(prop)
創(chuàng)建一個(gè) effect
边翁,用來命令 middleware
返回 saga
的上下文中的一個(gè)特定屬性。
Effect 組合器
race(effects)
創(chuàng)建一個(gè) Effect
描述信息硕盹,用來命令 middleware
在多個(gè) Effect
間運(yùn)行 競(jìng)賽(Race
)(與 Promise.race([...])
的行為類似)符匾。
import { take, call, race } from `redux-saga/effects`
import fetchUsers from './path/to/fetchUsers'
function* fetchUsersSaga {
const { response, cancel } = yield race({
response: call(fetchUsers),
cancel: take(CANCEL_FETCH)
})
}
all(effects)
創(chuàng)建一個(gè) Effect
描述信息,用來命令 middleware
并行地運(yùn)行多個(gè) Effect
瘩例,并等待它們?nèi)客瓿砂〗骸_@是與標(biāo)準(zhǔn)的 Promise#all
相當(dāng)對(duì)應(yīng)的 API
。
import { fetchCustomers, fetchProducts } from './path/to/api'
import { all, call } from `redux-saga/effects`
function* mySaga() {
const { customers, products } = yield all({
customers: call(fetchCustomers),
products: call(fetchProducts)
})
}
接口
Task
方法 | 返回值 |
---|---|
task.isRunning() | 若任務(wù)還未返回或拋出了一個(gè)錯(cuò)誤則為 true |
task.isCancelled() | 若任務(wù)已被取消則為 true |
task.result() | 任務(wù)的返回值垛贤。若任務(wù)仍在運(yùn)行中則為 undefined
|
task.error() | 任務(wù)拋出的錯(cuò)誤焰坪。若任務(wù)仍在執(zhí)行中則為 undefined
|
task.done | 一個(gè) Promise |
task.cancel() | 取消任務(wù)(如果任務(wù)仍在執(zhí)行中) |
Channel
方法 | 返回值 |
---|---|
Channel.take(callback) | 用于注冊(cè)一個(gè) taker |
Channel.put(message) | 用于在 buffer 上放入消息 |
Channel.flush(callback) | 用于從 channel 中提取所有被緩存的消息 |
Channel.close() | 關(guān)閉 channel,意味著不再允許做放入操作 |
Buffer
方法 | 返回值 |
---|---|
Buffer.isEmpty() | 如果緩存中沒有消息則返回 |
Buffer.put(message) | 用于往緩存中放入新的消息 |
Buffer.take() | 用于檢索任何被緩存的消息 |
SagaMonitor
用于由 middleware
發(fā)起監(jiān)視(monitor
)事件聘惦。實(shí)際上某饰,middleware
發(fā)起 5 個(gè)事件:
-
當(dāng)一個(gè)
effect
被觸發(fā)時(shí)(通過yield someEffect
),middleware
調(diào)用sagaMonitor.effectTriggered(options)
:options
包括:effectId
:Number
- 分配給yielded effect
的唯一ID
parentEffectId
:Number
- 父級(jí)Effect
的ID
善绎。在race
或parallel
effect
的情況下黔漂,所有在內(nèi)部yield
的effect
都將有一個(gè)直接race/parallel
的父級(jí)effect
。在最頂級(jí)的effect
的情況下禀酱,父級(jí)是包裹它的Saga
炬守。label
:String
- 在race effect
的情況下,所有子effect
都將被指定為傳遞給race
的對(duì)象中對(duì)應(yīng)鍵的標(biāo)簽比勉。effect
:Object
-yielded effect
其自身
-
如果該
effect
成功地被resolve
,則middleware
調(diào)用sagaMonitor.effectResolved(effectId, result)
effectId
:Number
-yielded effect
的ID
result
:any
- 該effect
成功resolve
的結(jié)果。在fork
或spawn
的情況下浩聋,結(jié)果將是一個(gè)Task
對(duì)象观蜗。
-
如果該
effect
因一個(gè)錯(cuò)誤被reject
,則middleware
調(diào)用sagaMonitor.effectRejected
effectId
:Number
-yielded effect
的ID
error
:any
- 該effect reject
的錯(cuò)誤
-
如果該
effect
被取消衣洁,則middleware
調(diào)用sagaMonitor.effectCancelled
-
effectId
:Number
-yielded effect
的ID
-
-
最后墓捻,當(dāng)
Redux action
被發(fā)起時(shí),middleware
調(diào)用sagaMonitor.actionDispatched
-
action
:Object
- 被發(fā)起的Redux action
坊夫。如果該action
是由一個(gè)Saga
發(fā)起的砖第,那么該action
將擁有一個(gè)屬性SAGA_ACTION
并被設(shè)為true
(你可以從redux-saga/utils
中導(dǎo)入SAGA_ACTION
)。
-
外部API
不做贅述环凿,可根據(jù)自己需要使用