3.5 compose redux sages
基于 redux-thunk 的实现特性,可以做到基于 promise 和递归的组合编排,而 redux-saga 提供了更容易的更高级的组合编排方式(当然这一切要归功于 Generator 特性),这一节的主要内容为:
基于 take Effect 实现更自由的任务编排
fork 和 cancel 实现非阻塞任务
Parallel 和 Race 任务
saga 组合 yield* saga
channels
3.5.1 基于 take Effect 实现更自由的任务编排
前面我们使用过 takeEvery helper,其实底层是通过 take effect 来实现的。通过 take effect 可以实现很多有趣的简洁的控制。
如果用 takeEvery 实现日志打印,我们可以用:
- import { takeEvery } from 'redux-saga'
- import { put,select } from 'redux-saga/effects'
- function* watchAndLog() {
- yield* takeEvery('*',function* logger(action) {
- const state = yield select()
- console.log('action',action)
- console.log('state after',state)
- })
- }
使用使用 take 过后可以改为:
- import { take } from 'redux-saga/effects'
- import { put,select } from 'redux-saga/effects'
- function* watchAndLog() {
- while (true) {
- const action = yield take('*')
- const state = yield select()
- console.log('action',state)
- }
- }
while(true) 的执行并非是死循环,而只是不断的生成迭代项而已,take Effect 在没有获取到对象的 action 时,会停止执行,直到接收到 action 才会执行后面的代码,然后重新等待
take 和 takeEvery 最大的区别在于 take 是主动获取 action ,相当于 action = getNextAction(),而 takeEvery 是消息推送。
基于主动获取的,可以做到更自由的控制,如下面的两个例子:
完成了三个任务后,提示恭喜
- import { take,put } from 'redux-saga/effects'
- function* watchFirstThreeTodosCreation() {
- for (let i = 0; i < 3; i++) {
- const action = yield take('TODO_CREATED')
- }
- yield put({type: 'SHOW_CONGRATULATION'})
- }
take 最不可思议的地方就是,将 异步的任务用同步的方式来编排 ,使用好 take 能极大的简化交互逻辑处理
3.5.2 fork 和 cancel 实现非阻塞任务
在提非阻塞之前肯定要先要说明什么叫阻塞的代码。我们看一下下面的例子:
- function* generatorFunction() {
- console.log('start')
- yield take('action1')
- console.log('take action1')
- yield call('api')
- console.log('call api')
- yield put({type: 'SOME_ACTION'})
- console.log('put blabla')
- }
因为 generator 的特性,必须要等到 take 完成才会输出 take action1,同理必须要等待 call api 完成才会输出 call api, 这就是我们所说的阻塞。
那阻塞会造成什么问题呢?见下面的例子:
一个登录的例子(这是一段有问题的代码,可以先研究一下这段代码问题出在哪儿)
- import { take,call,put } from 'redux-saga/effects'
- import Api from '...'
- function* authorize(user,password) {
- try {
- const token = yield call(Api.authorize,user,password)
- yield put({type: 'LOGIN_SUCCESS',token})
- return token
- } catch(error) {
- yield put({type: 'LOGIN_ERROR',error})
- }
- }
- function* loginFlow() {
- while (true) {
- const {user,password} = yield take('LOGIN_REQUEST')
- const token = yield call(authorize,password)
- if (token) {
- yield call(Api.storeItem,{token})
- yield take('logoUT')
- yield call(Api.clearItem,'token')
- }
- }
- }
我们先来分析一下 loginFlow 的流程:
通过 take effect 监听 login_request action
通过 call effect 来异步获取 token (call 不仅可以用来调用返回 Promise 的函数,也可以用它来调用其他 Generator 函数,返回结果为调用 Generator return 值)
-
成功(有 token)
1 过后异步存储 token 失败(token === undefined) 回到第 0 步
其中的问题:
一个隐藏的陷阱,在调用 authorize 的时候,如果用户点击了页面中的 logout 按钮将会没有反应(此时还没有执行 take('logoUT')),也就是被 authorize 阻塞了。
redux-sage 提供了一个叫 fork
的 Effect,可以实现非阻塞的方式,下面我们重新设计上面的登录例子:
- function* authorize(user,token})
- } catch(error) {
- yield put({type: 'LOGIN_ERROR',error})
- }
- }
- function* loginFlow() {
- while(true) {
- const {user,password} = yield take('LOGIN_REQUEST')
- yield fork(authorize,password)
- yield take(['logoUT','LOGIN_ERROR'])
- yield call(Api.clearItem('token'))
- }
- }
token 的获取放在了 authorize saga 中,因为 fork 是非阻塞的,不会返回值
authorize 中的 call 和 loginFlow 中的 take 并行调用
-
这里 take 了两个 action,take 可以监听并发的 action ,不管哪个 action 触发都会执行 call(Api.clearItem...) 并回到 while 开始
这个过程中的问题是如果用户触发 logout 了,没法停止 call api.authorize,并会触发 LOGIN_SUCCESS 或者 LOGIN_ERROR action 。
redux-saga 提供了 cancel Effect,可以 cancel 一个 fork task
- import { take,put,fork,cancel } from 'redux-saga/effects'
- // ...
- function* loginFlow() {
- while (true) {
- const {user,password} = yield take('LOGIN_REQUEST')
- // fork return a Task object
- const task = yield fork(authorize,password)
- const action = yield take(['logoUT','LOGIN_ERROR'])
- if (action.type === 'logoUT')
- yield cancel(task)
- yield call(Api.clearItem,'token')
- }
- }
cancel 的了某个 generator,generator 内部会 throw 一个错误方便捕获,generator 内部 可以针对不同的错误做不同的处理
- import { isCancelError } from 'redux-saga'
- import { take,token})
- return token
- } catch(error) {
- if(!isCancelError(error))
- yield put({type: 'LOGIN_ERROR',error})
- }
- }
3.5.3 Parallel 和 Race 任务
Parallel
基于 generator 的特性,下面的代码会按照顺序执行
- const users = yield call(fetch,'/users'),repos = yield call(fetch,'/repos')
为了优化效率,可以让两个任务并行执行
- const [users,repos] = yield [
- call(fetch,call(fetch,'/repos')
- ]
Race
某些情况下可能会对优先完成的任务进行处理,一个很常见的例子就是超时处理,当请求一个 API 超过多少时间过后执行特定的任务。
eg:
- import { race,take,put } from 'redux-saga/effects'
- import { delay } from 'redux-saga'
- function* fetchPostsWithTimeout() {
- const {posts,timeout} = yield race({
- posts: call(fetchApi,'/posts'),timeout: call(delay,1000)
- })
- if (posts)
- put({type: 'POSTS_RECEIVED',posts})
- else
- put({type: 'TIMEOUT_ERROR'})
- }
这里默认使用到了 race 的一个特性,如果某一个任务成功了过后,其他任务都会被 cancel 。
3.5.4 yield* 组合 saga
yield* 是 generator 的内关键字,使用的场景是 yield 一个 generaor。
yield* someGenerator 相当于把 someGenerator 的代码放在当前函数执行,利用这个特性,可以组合使用 saga
3.5.5 channels
通过 actionChannel 实现缓存区
先看如下的例子:
- import { take,... } from 'redux-saga/effects'
- function* watchRequests() {
- while (true) {
- const {payload} = yield take('REQUEST')
- yield fork(handleRequest,payload)
- }
- }
- function* handleRequest(payload) { ... }
这个例子是典型的 watch -> fork
,也就是每一个 REQEST 请求都会被并发的执行,现在如果有需求要求 REQUEST 一次只能执行一个,这种情况下可以使用到 actionChannel
通过 actionChannel 修改上例子
- import { take,actionChannel,... } from 'redux-saga/effects'
- function* watchRequests() {
- // 为 REQUEST 创建一个 actionChannel 相当于一个缓冲区
- const requestChan = yield actionChannel('REQUEST')
- while (true) {
- // 重 channel 中取一个 action
- const {payload} = yield take(requestChan)
- // 使用非阻塞的方式调用 request
- yield call(handleRequest,payload)
- }
- }
- function* handleRequest(payload) { ... }
channel 可以设置缓冲区的大小,如果只想处理最近的5个 action 可以如下设置
- import { buffers } from 'redux-saga'
- const requestChan = yield actionChannel('REQUEST',buffers.sliding(5))
eventChannel 和外部事件连接起来
eventChannel 不同于 actionChannel,actionChannel 是一个 Effect ,而 eventChannel 是一个工厂函数,可以创建一个自定义的 channel
下面创建一个倒计时的 channel 工厂
- import { eventChannel,END } from 'redux-saga'
- function countdown(secs) {
- return eventChannel(emitter => {
- const iv = setInterval(() => {
- secs -= 1
- if (secs > 0) {
- emitter(secs)
- } else {
- // 结束 channel
- emitter(END)
- clearInterval(iv)
- }
- },1000);
- // 返回一个 unsubscribe 方法
- return () => {
- clearInterval(iv)
- }
- }
- )
- }
通过 call 使用创建 channel
- export function* saga() {
- const chan = yield call(countdown,value)
- try {
- while (true) {
- // take(END) 会导致直接跳转到 finally
- let seconds = yield take(chan)
- console.log(`countdown: ${seconds}`)
- }
- } finally {
- // 支持外部 cancel saga
- if (yield cancelled()) {
- // 关闭 channel
- chan.close()
- console.log('countdown cancelled')
- } else {
- console.log('countdown terminated')
- }
- }
- }
通过 channel 在 saga 之间通信
除了 eventChannel 和 actionChannel,channel 可以不用连接任何事件源,直接创建一个空的 channel,然后手动的 put 事件到 channel 中
以上面的 watch->fork 为基础,需求改为 ,需要同时并发 3 个request 请求执行:
- import { channel } from 'redux-saga'
- import { take,... } from 'redux-saga/effects'
- function* watchRequests() {
- // 创建一个空的 channel
- const chan = yield call(channel)
- // fork 3 个 worker saga
- for (var i = 0; i < 3; i++) {
- yield fork(handleRequest,chan)
- }
- while (true) {
- // 等待 request action
- const {payload} = yield take('REQUEST')
- // put payload 到 channel 中
- yield put(chan,payload)
- }
- }
- function* handleRequest(chan) {
- while (true) {
- const payload = yield take(chan)
- // process the request
- }
- }