007 Redux-saga
# 简介
Redux-saga 与 Redux-Thunk 最大的不同在于,redux-saga又回到了纯对象模式,并且通过监听的方式来执行副作用函数。而 redux-thunk 则直接执行副作用函数,异步请求结束后直接发起action,并且与 Redux-Thunk 相比,Redux-Saga 的好处是你可以更好得控制业务流程,增强了代码的可读性,在 saga 之前,你可能会在 action creator 里处理业务逻辑,虽然能跑通,但是难以测试。比如:
// action creator with thunking
function createRequest () {
return (dispatch, getState) => {
dispatch({ type: 'REQUEST_STUFF' });
someApiCall(function(response) {
// some processing
dispatch({ type: 'RECEIVE_STUFF' });
});
};
}
// ui
function onHandlePress () {
this.props.dispatch({ type: 'SHOW_WAITING_MODAL' });
this.props.dispatch(createRequest());
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
这样通过 redux state 和 reducer 把所有的事情串联到起来。
但问题是:
Code is everywhere.
# saga
通过 saga,你只需要触发一个 action 。
function onHandlePress () {
// createRequest 触发 action `BEGIN_REQUEST`
this.props.dispatch(createRequest());
}
2
3
4
然后所有后续的操作都通过 saga 来管理。
function *hello() {
// 等待 action `BEGIN_REQUEST`
yield take('BEGIN_REQUEST');
// dispatch action `SHOW_WAITING_MODAL`
yield put({ type: 'SHOW_WAITING_MODAL' });
// 发布异步请求
const response = yield call(myApiFunctionThatWrapsFetch);
// dispatch action `PRELOAD_IMAGES`, 附上 response 信息
yield put({ type: 'PRELOAD_IMAGES', response.images });
// dispatch action `HIDE_WAITING_MODAL`
yield put({ type: 'HIDE_WAITING_MODAL' });
}
2
3
4
5
6
7
8
9
10
11
12
可以看出,调整之后的代码有几个优点:
- 所有业务代码都存于 saga 中,不再散落在各处
- 全同步执行,就算逻辑再复杂,看起来也不会乱
总结以下就是 Thunk 比较适合一些小项目,操作简单,逻辑包含在函数内部,不需要学习Generator函数及操作流程,学习成本较低,应该根据项目的复杂度决定使用何种中间件。
# 使用
使用 saga 最重要的一点是监听,网上很多文章都把这一点省略掉了,根据下图实现一个简单的加减操作

# ui
import React from "react";
import { connect } from "react-redux";
function Hello(props) {
return (
<div>
<span>{props.number}</span>
<button onClick={props.handleAdd}>+</button>;
</div>
);
}
const mapStateToProps = (state) => {
return {
number: state.reducer2.number
};
};
const mapDispatchToProps = {
handleAdd: () => ({ type: "ADD_NUMBER" }) // 这个 action 将被saga拦截
};
export default connect(mapStateToProps, mapDispatchToProps)(Hello);
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# saga
function* addFun() {
yield put({ type: "ADD_NUMBER_FOR_REDUCER" }); // 发起派生action用于更新state
}
function* watchAdd() {
yield takeEvery("ADD_NUMBER", addFun); // 被监听到了,对应执行 addFun 函数
}
export function* root() {
yield all([ // 使用all运行多个Effect,等待监听结果
watchAdd()
]);
}
2
3
4
5
6
7
8
9
10
11
# reducer
const defaultState = {
number: 0
};
export default function reducer(state = defaultState, action) {
switch (action.type) {
case "ADD_NUMBER_FOR_REDUCER":
return { ...state, number: state.number + 1 };
default:
return state;
}
}
2
3
4
5
6
7
8
9
10
11
12
# 启动与监听
import { createStore, applyMiddleware } from "redux";
import createSagaMiddleware from "redux-saga";
import { root } from "./sagas";
import reducers from "./reducers";
const sagaMiddleware = createSagaMiddleware();
export const store = createStore(reducers, applyMiddleware(sagaMiddleware));
sagaMiddleware.run(root); // 由 sagas.js 导出root进行启动与监听
2
3
4
5
6
7
# Api
saga 还内置了很多很实用的api,包括防抖、节流、延迟、取消 等
# 常用辅助函数
takeEvery (pattern, saga, ...args)
- pattern: 需要监听的值 如: “TEST”
- saga: 需要执行的对应函数
- args: 需要传给函数的参数
takeLatest (pattern, saga, ...args)
使用方法与takeEvery基本相同,不同的是takeLatest只会执行会后一次任务,如果上一次已经启动就会取消上一次的运行。
throttle (ms, pattern, saga, ...args) 节流
# 常用 Effect Api
take(pattern)
while(true) { let one = yield take('TEST') yield put({type:'TESTXXX',value}) }1
2
3
4take方法类似于一次性使用得所以经常和while搭配,可以保持一直监听得状态,但是又可以有效的控制流程
put(action): 创建一个Effect,用来命令中间件向store发起该action,这个action是非阻塞的。相当于
dispatch(action)call(fn, ...args) : call方法调用fn,参数为args,返回一个描述对象
fork(fn, ...args): 与call 大致相同,但为 非阻塞调用 的形式执行
fncancel(task): 用来命令 middleware 取消之前的一个任务
select(selector, ...args): 用于获取Store 的 state
# Effect 组合器
cancel() 取消
某个任务呗
cancel取消时,处理取消逻辑写在finally中function* forkTask() { try { // 2.1,延迟2s控制台打印 'forkTask finished' yield delay(2000) console.log('forkTask finished'); } catch (error) { // 2.2,如果出错则控制台打印 'error' console.log('error'); } finally { // 2.3,forkTask不管以怎样形式结束都将执行finally区块内部内容 console.log('当task以任意方式结束,不管是正常结束,抛错结束,还是当前任务取消结束,finnally都将执行'); } } function* cancelFork() { // 2,cancelFork使用fork启动一个非阻塞的任务forkTask const task = yield fork(forkTask) // 3,延迟1s yield delay(1000) // 4,取消任务forkTask yield cancel(task) } function* rootSaga() { // 1,根saga启动cancelFork任务 yield call(cancelFork) } export default rootSaga1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27all() 并发
当
all参数中所有任务全部完成,all所在的Generator函数才会恢复执行。而如果参数中某个任务失败且该任务未对错误进行处理,那么错误将冒泡到all所在的Generator中,且取消其他任务race()
race方法类似于Promise.race,即race参数中多个任务竞赛,谁先完成,race就结束,这里也分两种情况:1,如果率先完成者正常完成,则取消其他未完成的任务,且完成任务结果时该任务return值,其他取消任务的结果均为undefined。 2,率先完成任务失败(抛错且未处理),则错误冒泡到race所在Generator函数中,且取消其他竞赛中的任务。
# 阻塞与非阻塞
| function | block |
|---|---|
| take | 阻塞 |
| call | 阻塞 |
| all | 不一定 |
| putResolve | 阻塞 |
| join | 阻塞 |
| put | 非阻塞 |
| fork | 非阻塞 |
| cancel | 非阻塞 |
| cps | 非阻塞 |
# 非阻塞并非异步
redux-saga中put()非阻塞意思是假如这个action中有中间件,或一些异步操作造成了store信息更新不及时,那么effects中并不会等着这些操作执行完,即会继续执行接下来的操作
# 总结
redux-saga优点:
- 异步解耦: 异步操作被被转移到单独 saga.js 中,不再是掺杂在 action.js 或 component.js 中
- action摆脱thunk function:dispatch 的参数依然是一个纯粹的 action (FSA),而不是充满 “黑魔法” thunk function
- 异常处理:受益于 generator function 的 saga 实现,代码异常/请求失败 都可以直接通过 try/catch 语法直接捕获处理
- 功能强大:redux-saga提供了大量的Saga 辅助函数和Effect 创建器供开发者使用,开发者无须封装或者简单封装即可使用
- 灵活:redux-saga可以将多个Saga可以串行/并行组合起来,形成一个非常实用的异步flow
- 易测试:提供了各种case的测试方案,包括mock task,分支覆盖等等
redux-saga缺陷:
- 额外的学习成本: redux-saga不仅在使用难以理解的 generator function,而且有数十个API,学习成本远超redux-thunk,最重要的是你的额外学习成本是只服务于这个库的,与redux-observable不同,redux-observable虽然也有额外学习成本但是背后是rxjs和一整套思想
- 体积庞大: 体积略大,代码近2000行,min版25KB左右
- 功能过剩: 实际上并发控制等功能很难用到,但是我们依然需要引入这些代码
- ts支持不友好: yield无法返回TS类型
参考:
Redux-Thunk vs. Redux-Saga (opens new window)