Redux-saga
概述
在 redux 一文中我们有说过处理异步我们应该放在 reducer 之前,所以我们需要中间件来处理我们的异步操作。redux-saga 就是一个用于管理 redux 应用的异步操作中间件,redux-saga 通过创建 sagas 将所有异步操作的逻辑都收集在一个位置集中处理。
简单的来说就是两步:
-
sagas负责协调那些复杂或者异步的操作 -
reducer负责处理action的stage更新
数据流
我们先看一张图来大致了解下整个过程:

有了之前 redux 的基础,这张图其实还是比较容易理解的,下面我们来简单说下:
-
React组件由用户触发事件,通过action creator发起action -
sagas监听发起的action,然后决定基于这个action做什么 - 紧接着根据上步所做处理将
effects(sage的任务单元,简单的JavaScript对象)传给reducer - 最后由
reducer返回新的state
安装
yarn add redux-saga
// npm install redux-saga -S
使用实例
配置
import { createStore, combineReducers, applyMiddleware } from 'redux';
import global from './reducers/global';
import login from './reducers/login';
import createSagaMiddleware from 'redux-saga'; // 引入redux-saga中的createSagaMiddleware函数
import rootSaga from '../sagas'; // 引入saga.js
const sagaMiddleware = createSagaMiddleware() // 执行
const reducerAll = {
global,
login
}
export const store = createStore(
combineReducers({...reducerAll}), // 合并reducer
applyMiddleware(sagaMiddleware) // 中间件,加载sagaMiddleware
)
sagaMiddleware.run(rootSaga)
ui组件触发action创建函数
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创建函数返回的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
一个 effect 就是一个纯文本 javascript 对象,包含一些将被 saga middleware 执行的指令。那么如何创建 effect 呢?使用下面我们马上要讲到API中 redux-saga 提供的工厂函数来创建 effect
Task
一个 task 就像是一个在后台运行的进程。在基于 redux-saga 的应用程序中,可以同时运行多个 task。通过 fork 函数来创建 task:
阻塞调用/非阻塞调用
阻塞调用的意思是,Saga 在 yield Effect 之后会等待其执行结果返回,结果返回后才会恢复执行 Generator 中的下一个指令。
非阻塞调用的意思是,Saga 会在 yield Effect 之后立即恢复执行。
function* saga() {
yield take(ACTION) // 阻塞: 将等待 action
yield call(ApiFn, ...args) // 阻塞: 将等待 ApiFn (如果 ApiFn 返回一个 Promise 的话)
yield call(otherSaga, ...args) // 阻塞: 将等待 otherSaga 结束
yield put(ACTION) // 非阻塞
yield put(channel, ACTION) // 当 put 没有被缓存而是被 taker 立即消费掉时,阻塞
const task = yield fork(otherSaga, ...args) // 非阻塞: 将不会等待 otherSaga
yield cancel(task) // 非阻塞: 将立即恢复执行
// or
yield join(task) // 阻塞: 将等待 task 结束
}
Watcher/Worker
指的是一种使用两个单独的 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
}
Middleware API
createSagaMiddleware(options)
创建一个 Redux middleware,并将 Sagas 连接到 Redux Store。
options : Object - 传递给 middleware 的选项列表。
middleware.run(saga, ...args)
动态执行 saga。用于 applyMiddleware 阶段之后执行 Sagas。这个方法返回一个
Task 描述对象。
Saga辅助函数
takeEvery
在发起(dispatch)到 Store 并且匹配 pattern 的每一个 action 上派生一个 saga。简单来说就是监听所有的匹配到的 action。
import { takeEvery } from `redux-saga/effects`
function* fetchUser(action) {
...
}
function* watchFetchUser() {
yield takeEvery('USER_REQUESTED', fetchUser)
}
takeLatest
在发起到 Store 并且匹配 pattern 的每一个 action 上派生一个 saga。并自动取消之前所有已经启动但仍在执行中的 saga 任务。
import { takeLatest } from `redux-saga/effects`
function* fetchUser(action) {
...
}
function* watchLastFetchUser() {
yield takeLatest('USER_REQUESTED', fetchUser)
}
这样便可以保证:即使用户以极快的速度连续多次触发 USER_REQUESTED action,我们都只会以最后的一个结束。
takeLeading
在发起到 Store 并且匹配 pattern 的每一个 action 上派生一个 saga。 它将在派生一次任务之后阻塞,直到派生的 saga 完成,然后又再次开始监听指定的 pattern。
import { takeLeading } from `redux-saga/effects`
function* fetchUser(action) {
...
}
function* watchLastFetchUser() {
yield takeLeading('USER_REQUESTED', fetchUser)
}
由于 takeLeading 在其开始之后便无视所有新传入的任务,我们便可以保证:如果用户以极快的速度连续多次触发 USER_REQUESTED action,我们都只会保持以第一个 action 运行。
throttle
它在派生一次任务之后,仍然将新传入的 action 接收到底层的 buffer 中,至多保留(最近的)一个。但与此同时,它在 ms 毫秒内将暂停派生新的任务 —— 这也就是它被命名为节流阀(throttle)的原因。其用途,是在处理任务时,无视给定的时长内新传入的 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 无视了一段时间内连续的 FETCH_AUTOCOMPLETE,我们便可以确保用户不会因此向我们的服务器发起大量请求。
Effect 创建器
注意:
- 以下每个函数都会返回一个普通 Javascript 对象(plain JavaScript > > object),并且不会执行任何其它操作。
- 执行是由 middleware 在上述迭代过程中进行的。
- middleware 会检查每个 Effect 的描述信息,并进行相应的操作
take
创建一个 Effect 描述信息,用来命令 middleware 在 Store 上等待指定的 action。 在发起匹配的 action 之前,Generator 将暂停。
put
创建一个 Effect 描述信息,用来命令 middleware 向 Store 发起一个 action。 这个 effect 是非阻塞型的,并且所有向下游抛出的错误(例如在 reducer 中),都不会冒泡回到 saga 当中。
call(fn, ...args)
创建一个 Effect 描述信息,用来命令 middleware 以参数 args 调用函数 fn 。
fork(fn, ...args)
创建一个 Effect 描述信息,用来命令 middleware 以 非阻塞调用 的形式执行 fn。
join(task)
创建一个 Effect 描述信息,用来命令 middleware 等待之前的一个分叉任务的结果。
cancel(task)
创建一个 Effect 描述信息,用来命令 middleware 取消之前的一个分叉任务。
select(selector, ...args)
创建一个 Effect,用来命令 middleware 在当前 Store 的 state 上调用指定的选择器(即返回 selector(getState(), ...args) 的结果)
例如,假设我们在应用程序中有这样结构的一份 state:
state = {
cart: {...}
}
我们创建一个 选择器(selector),即一个知道如果从 State 中提取 cart 数据的函数:
// selectors.js
export const getCart = state => state.cart
然后,我们可以使用 select Effect 从 Saga 的内部使用该选择器:
import { take, fork, select } from 'redux-saga/effects'
import { getCart } from './selectors'
function* checkout() {
// 使用被导出的选择器查询 state
const cart = yield select(getCart)
// ... 调用某些 API,然后发起一个 success/error action
}
export default function* rootSaga() {
while (true) {
yield take('CHECKOUT_REQUEST')
yield fork(checkout)
}
}
actionChannel(pattern, [buffer])
创建一个 Effect,用来命令 middleware 通过一个事件 channel 对匹配 pattern 的 action 进行排序。 作为可选项,你也可以提供一个 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)
创建一个 Effect,用来命令 middleware 从 channel 中冲除所有被缓存的数据。被冲除的数据会返回至 saga,这样便可以在需要的时候再次被利用。
function* saga() {
const chan = yield actionChannel('ACTION')
try {
while (true) {
const action = yield take(chan)
// ...
}
} finally {
const actions = yield flush(chan)
// ...
}
}
cancelled()
创建一个 Effect,用来命令 middleware 返回该 generator 是否已经被取消。通常你会在 finally 区块中使用这个 Effect 来运行取消时专用的代码。
function* saga() {
try {
// ...
} finally {
if (yield cancelled()) {
// 只应在取消时执行的逻辑
}
// 应在所有情况下都执行的逻辑(例如关闭一个 channel)
}
}
setContext(props)
创建一个 effect,用来命令 middleware 更新其自身的上下文。这个 effect 扩展了 saga 的上下文,而不是代替。
getContext(prop)
创建一个 effect,用来命令 middleware 返回 saga 的上下文中的一个特定属性。
Effect 组合器
race(effects)
创建一个 Effect 描述信息,用来命令 middleware 在多个 Effect 间运行 竞赛(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)
创建一个 Effect 描述信息,用来命令 middleware 并行地运行多个 Effect,并等待它们全部完成。这是与标准的 Promise#all 相当对应的 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() | 若任务还未返回或抛出了一个错误则为 true |
| task.isCancelled() | 若任务已被取消则为 true |
| task.result() | 任务的返回值。若任务仍在运行中则为 undefined
|
| task.error() | 任务抛出的错误。若任务仍在执行中则为 undefined
|
| task.done | 一个 Promise |
| task.cancel() | 取消任务(如果任务仍在执行中) |
Channel
| 方法 | 返回值 |
|---|---|
| Channel.take(callback) | 用于注册一个 taker |
| Channel.put(message) | 用于在 buffer 上放入消息 |
| Channel.flush(callback) | 用于从 channel 中提取所有被缓存的消息 |
| Channel.close() | 关闭 channel,意味着不再允许做放入操作 |
Buffer
| 方法 | 返回值 |
|---|---|
| Buffer.isEmpty() | 如果缓存中没有消息则返回 |
| Buffer.put(message) | 用于往缓存中放入新的消息 |
| Buffer.take() | 用于检索任何被缓存的消息 |
SagaMonitor
用于由 middleware 发起监视(monitor)事件。实际上,middleware 发起 5 个事件:
-
当一个
effect被触发时(通过yield someEffect),middleware调用sagaMonitor.effectTriggered(options):options包括:effectId:Number- 分配给yielded effect的唯一IDparentEffectId:Number- 父级Effect的ID。在race或paralleleffect的情况下,所有在内部yield的effect都将有一个直接race/parallel的父级effect。在最顶级的effect的情况下,父级是包裹它的Saga。label:String- 在race effect的情况下,所有子effect都将被指定为传递给race的对象中对应键的标签。effect:Object-yielded effect其自身
-
如果该
effect成功地被resolve,则middleware调用sagaMonitor.effectResolved(effectId, result)effectId:Number-yielded effect的IDresult:any- 该effect成功resolve的结果。在fork或spawn的情况下,结果将是一个Task对象。
-
如果该
effect因一个错误被reject,则middleware调用sagaMonitor.effectRejectedeffectId:Number-yielded effect的IDerror:any- 该effect reject的错误
-
如果该
effect被取消,则middleware调用sagaMonitor.effectCancelled-
effectId:Number-yielded effect的ID
-
-
最后,当
Redux action被发起时,middleware调用sagaMonitor.actionDispatched-
action:Object- 被发起的Redux action。如果该action是由一个Saga发起的,那么该action将拥有一个属性SAGA_ACTION并被设为true(你可以从redux-saga/utils中导入SAGA_ACTION)。
-
外部API
不做赘述,可根据自己需要使用