Redux Saga
Redux-Saga是什么?
Redux-saga是一个Redux Middleware
Redux-saga是用来做什么的?
是用来更优雅地管理side effects的。
那什么是side effects?
Side effects are the most common way that a program interacts with the outside world (people, filesystems, other computers on networks) [from Wikipedia]
side effect是程序和外界(人,文件系统,网上的其他电脑)交互最常见的一个方式。
对于前端来讲,side effect一般指异步网络请求。
那effect又是什么
An effect is a plain JavaScript Object containing some instructions to be executed by the saga middleware.
一个effect是个普通的JS对象,包含一些会被中间件saga执行的指令。
在redux-saga的世界里,所有的effect都必须被yield
才会执行。而且原则上来讲,所有的yield
后面也只能跟随着effect,以保证代码的易测性。比如:
yield fetch(UrlMap.fetchData);
// 应该用call:
yield call(fetch, UrlMap.fetchData);
task是什么
task是generator方法的执行环境,所有的saga的generator方法都跑在task里面。
redux-saga的优点都有什么
用于管理前端异步网络请求,本质上就是为了解决异步action的问题。
- side effects转移到单独的saga.js里面,不再掺杂于action.js里面,保持action的简单纯粹,又使得异步操作可以被集中处理。
- saga的机制在处理复杂的问题上更顺手,有更加细腻的控制流(所有的saga都可以被中断)。
- 对比thunk,dispatch的参数依然是一个纯粹的action
- 每一个saga都是个generator function,代码可以采用同步书写的方式处理异步逻辑,解决了backcall hell问题,代码易读易懂。
- 受益于generator function的saga实现,代码异常or请求失败都可以通过try/catch来捕获处理。
如何使用redux-saga
举例简单的一个hello saga
// 单独的文件:sagas.js, 统一管理副作用:
export function* helloSaga() {
console.log('Hello Sagas!');
}
// 将saga和store关联起来, 入口文件 main.js
import { createStore, applyMiddleware } from 'redux';
import createSagaMiddleware from 'redux-saga';
import helloSaga from './sagas'
import rootReducer from './reducers'
// 创建 saga middleware
const sagaMiddleware = createSagaMiddleware();
// 创建 store
const store = createStore(
rootReducer,
applyMiddleware(sagaMiddleware) // 注入 saga middleware
);
// 启动 saga
sagaMiddleware.run(helloSaga);
// 省略ReactDOM.render部分的代码...
这样就能看到Hello Sagas!
了。
代码分析:
Line 8 - 9: 通过redux-saga提供的工厂函数createSagaMiddleware创建sagaMiddleware(当然创建时候也可以传递一些可选的配置参数)
Line 11 - 15: 创建store instance,注入saga中间件。意味着,以后每次运行store.dispatch(action)的时候,数据流都会经过sagaMiddleware这一道工序,进行必要的"加工处理"(比如发送一个异步请求)。
Line 17 - 18: 启动saga,调用run方法使得generator可以开始执行,也就是执行rootSaga。通常是程序的一些初始化操作(比如:初始化数据,注册action监听)。
再看另一个例子,有三个按钮,第一个按一下1秒后计数器会显示+1,第二个按钮按一下+1,第三个按钮按一下-1。
reducer里面有加一的处理:
...
case 'INCREMENT':
return {
...state,
count: state.count + 1
}
...
sagas.js
import { all, put, takeEvery } from 'redux-saga/effects';
const delay = (ms) => new Promise(res => setTimeout(res, ms));
// worker Saga: 执行异步的 increment 任务
export function * incrementAsync () {
yield delay(1000); // middleware 拿到一个 yield 后的 Promise,暂停1s后再继续执行
yield put({ type: 'INCREMENT' }); // 告诉 middleware 发起一个 INCREMENT 的 action。
}
// watcher Saga: 在每个INCREMENT_ASYNC上生成一个新的incrementAsync任务
export function * watchIncrementAsync () {
yield takeEvery('INCREMENT_ASYNC', incrementAsync);
}
// 启动saga们
export default function * rootSaga () {
yield all([
watchIncrementAsync(),
helloSaga()
]);
}
和前面的例子相似,只是把helloSaga换成rootSaga即可。
代码分析:
Sagas 是被实现为 Generator functions 的
Line 2: 创建一个delay
函数,返回一个Promise,它在指定的毫秒数后解析。
Line 5 - 8:incrementAsync
这个 Saga 会暂停,直到 delay 返回的 Promise 被 resolve,即 1000ms 之后;
Line 6: middleware 拿到一个 yield 后的 Promise,middleware 暂停 Saga,直到 Promise 完成。一旦 Promise 被 \resolve,middleware 会恢复 Saga 接着执行,直到遇到下一个 yield。
Line 7: 这里就是第二个yield啦,这里的put({type: 'INCREMENT'})
就是一个Effect,Effect 是纯js对象,其中包含了给 middleware 执行的指令;当 middleware 拿到被Saga yield的Effect的时候,也会暂停Saga,直到Effect 执行完成,然后Saga 会再次被恢复。
Line 11 - 13: 写一个watcher saga,用redux-saga的api takeEvery 来监听所有的 INCREMENT_ASYNC action,并在 action 被匹配时执行 incrementAsync 任务。
Line 15 - 18: 有了Saga,,现添加一个rootSaga来负责启动所有Saga,用了all api,如果有其他Saga都能一起启动。