在学习redux-saga之前,我们必须了解ES6 Generators [ES6 Generators]
介绍
中间件:将具体业务和底层逻辑解耦的组件。redux中,中间件就是一个函数,对store.dispatch方法进行了改造,在发出 Action 和执行 Reducer 这两步之间,添加了其他功能。
redux应用中最常用的两种异步流处理方式: redux-thunk,redux-saga
redux-saga 是一个用于管理 Redux 应用异步操作(Side Effects。译注:直译成 “副作用” 不太通顺,所以这里译为 “异步操作” 更好理解)的中间件(又称异步 action)。 redux-saga 通过创建 Sagas 将所有的异步操作逻辑收集在一个地方集中处理,可以用来代替 redux-thunk 中间件。
它具有如下特性:
集中处理 redux 副作用问题。
被实现为 generator 。
类 redux-thunk 中间件。
watch/worker(监听->执行) 的工作形式。
如何使用
- 创建与运行
- 测试(遍历 generator 并对它的值(
gen.next() // => { done: ..., value: ... }
)做 deepEqual 测试)
redux-saga [官方文档]有详细的使用说明
redux-saga [中文文档]
概念
使用 Saga 辅助函数
(常用)
- Middleware API
- createSagaMiddleware(...sagas)
- middleware.run(saga, ...args)
Saga Helpers
- takeEvery(pattern, saga, ...args) :接受Action,并且触发某个方法(一個 Generator function) 实例同时启动
- takeLatest(pattern, saga, ..args) :多次触发,去最后一次
Effect creators
- take(pattern) :创建一条 Effect 描述信息,指示 middleware 等待 Store 上指定的 action。 Generator 会暂停,直到一个与 pattern 匹配的 action 被发起。
- put(action) : put(action) 用于触发 action,功能上类似于dispatch。
- fork(fn, ...args) :创建一条 Effect 描述信息,指示 middleware 以 无阻塞调用 方式执行 fn。fork 类似于 call,可以用来调用普通函数和 Generator 函数。但 fork 的调用是无阻塞的,在等待 fn 返回结果时,middleware 不会暂停 Generator。 相反,一旦 fn 被调用,Generator 立即恢复执行。 fork 与 race 类似,是一个中心化的 Effect,管理 Sagas 间的并发。
- call(fn, ...args) :cal阻塞型调用,有阻塞地调用 saga 或者返回 promise 的函数。
- join(task): 创建一个效果描述,指示中间件等待先前分叉的任务的结果。
- cancel(task): 是一个无阻塞 Effect。也就是说,Generator 将在取消异常被抛出后立即恢复。
- select(selector, ...args) : 作用和 redux thunk 中的 getState 相同。
const id = yield select(state => state.id);
Effect combinators
- race(effects) :rece Effect提供了一个方法,在多个Effects之间触发一个竞赛。在race Effect中,所有参与竞赛的任务,除了优胜者,其他任务都会被取消
Interfaces
- Task :Task 接口指定了通过
fork
,middleware.run
或runSaga
执行 Saga 的结果。
- Task :Task 接口指定了通过
redux-saga API详解可以查看 [API 参考]
声明式 Effects
Effect 是一个简单的对象,该对象包含了一些给 middleware 解释执行的信息。可以通过使用 effects API 如 fork,call,take,put,cancel
等来创建 Effect。( redux-saga [API 参考])
触发一个action 到 reducer的过程中,如 yield call(delay, 1000)
即 yield 了下面的对象,call 创建了一条描述结果的信息,然后,redux-saga middleware
将确保执行这些指令并将指令的结果返回给 Generator
从 Saga 内触发异步操作(Side Effect)总是由 yield 一些声明式的 Effect 来完成的 (你也可以直接 yield Promise,但是这会让测试变得困难。使用 Effect 诸如 call 和 put,与高阶 API 如 takeEvery 相结合,又有额外的易于测试的好处。
发起action
redux-saga 提供了另外一个函数 put,这个函数用于创建 dispatch Effect
-
yield put({ type: '',... })
yield关键字用来暂停和恢复一个生成器函数,yield关键字使生成器函数执行暂停,yield关键字后面的表达式的值返回给生成器的调用者。它可以被认为是一个基于生成器的版本的return关键字。
yield关键字实际返回一个IteratorResult对象,它有两个属性,value和done。value属性是对yield表达式求值的结果,而done是false,表示生成器函数尚未完全完成。
错误处理
try/catch
语法在 Saga 中捕获错误
redux-saga 是如何工作的?
Redux Saga可以理解为一个和系统交互的常驻进程,其中,Saga可简单定义如下:
- Saga = Worker + Watcher
Sagas通过Generator函数来创建,采用 Generator 函数来 yield Effects,只会在应用启动时调用,我们可以把它看作是在后台运行的进程,Sagas监听发起的action,然后决定基于这个action来做什么,是发起异步调用还是发起其他的action到Store,甚至是调用其他的Sagas
- sagas 包含3个部分,用于联合执行任务:
Watcher/Worker 指的是一种使用两个单独的 Saga 来组织控制流的方式。
1. root saga:立即启动 sagas 的唯一入口
2. Watcher: 监听发起的 action 并在每次接收到 action 时 fork 一个 worker。
3. Worker: 处理 action 并结束它。
eg:
//动态执行 rootSaga。用于 applyMiddleware 阶段之后执行 rootSaga。
middleware.run(rootSaga, ...args)
...
//Watcher/Worker
function* watcher() {
while(true) {
const action = yield take(ACTION)
yield fork(worker, action.payload)
}
}
function* worker(payload) {
// ... do some stuff
}
VS redux-thunk
- 使用redux-thunk
try { dispatch({ type: LOGIN_REQUEST }); let { data } = await request.post('/login', { user, password }); await dispatch(loadUserData(data.uid)); dispatch({ type: LOGIN_SUCCESS, data }); } catch(error) { dispatch({ type: LOGIN_ERROR, error }); } } 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 }); } }
- 使用redux-saga
export function* loginSaga() { while(true) { const { user, pass } = yield take(LOGIN_REQUEST) //等待 Store 上指定的 action LOGIN_REQUEST try { let { data } = yield call(request.post, '/login', { user, pass }); //阻塞,请求后台数据 yield fork(loadUserData, data.uid); //非阻塞执行loadUserData yield put({ type: LOGIN_SUCCESS, data }); //发起一个action,类似于dispatch } catch(error) { yield put({ type: LOGIN_ERROR, error }); } } } export function* loadUserData(uid) { try { yield put({ type: USERDATA_REQUEST }); let { data } = yield call(request.get, `/users/${uid}`); yield put({ type: USERDATA_SUCCESS, data }); } catch(error) { yield put({ type: USERDATA_ERROR, error }); } }
- 相比Redux Thunk,使用Redux Saga有几处明显的变化:
1. 在组件中,不再dispatch(action creator),而是dispatch(pure action)
2. 组件中不再关注由谁来处理当前action,action经由root saga分发
3. 具体业务处理方法中,通过提供的call/put等帮助方法,声明式的进行方法调用
4. 使用ES6 Generator语法,简化异步代码语法 5. redux-saga 将异步任务进行了集中处理,且方便测试。
结论
redux-saga的使用更多的是根据个人需求与习惯
redux-saga 的优点:
1. 声明式 Effects:所有的操作以JavaScript对象的方式被 yield,并被 middleware 执行。使得在 saga 内部测试变得更加容易,可以通过简单地遍历 Generator 并在 yield 后的成功值上面做一个 deepEqual 测试。
2. 高级的异步控制流以及并发管理:可以使用简单的同步方式描述异步流,并通过 fork 实现并发任务。
3. 架构上的优势:将所有的异步流程控制都移入到了 sagas,UI 组件不用执行业务逻辑,只需 dispatch action 就行,增强组件复用性。
目前在项目实践中遇到的一些问题:
1. redux-saga 不强迫我们捕获异常,这往往会造成异常发生时难以发现原因。因此,一个良好的习惯是,相信任何一个过程都有可能发生异常。
2. generator 的调试环境比较糟糕,babel 的 source-map 经常错位,经常要手动加 debugger 来调试。
4. 在action的定义上要谨慎,避免action在saga和reducer之间重复触发,造成死循环