基于 redux-thunk 的实现特性,可以做到基于 promise 和递归的组合编排,而 redux-saga 提供了更容易的更高级的组合编排方式(当然这一切要归功于 Generator 特性),这一节的主要内容为:

  1. 基于 take Effect 实现更自由的任务编排

  2. fork 和 cancel 实现非阻塞任务

  3. Parallel 和 Race 任务

  4. saga 组合 yield* saga

  5. 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', action)
console.log('state after', 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'})
}

登录和登出逻辑可以放在同一个函数内共享变量

function* loginFlow() {
while (true) {
yield take('LOGIN')
// ... perform the login logicyield take('LOGOUT')
// ... perform the logout logic
}
}

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, user, password)
if (token) {
yield call(Api.storeItem, {token})
yield take('LOGOUT')
yield call(Api.clearItem, 'token')
}
}
}

我们先来分析一下 loginFlow 的流程:

  1. 通过 take effect 监听 login_request action

  2. 通过 call effect 来异步获取 token (call 不仅可以用来调用返回 Promise 的函数,也可以用它来调用其他 Generator 函数,返回结果为调用 Generator return 值)

  3. 成功(有 token) 1 过后异步存储 token

    1. 等待 logout action

    2. logout 事件触发后异步清除 token

    3. 然后回到第 0 步

  4. 失败(token === undefined) 回到第 0 步

其中的问题:

一个隐藏的陷阱,在调用 authorize 的时候,如果用户点击了页面中的 logout 按钮将会没有反应(此时还没有执行 take('LOGOUT')) , 也就是被 authorize 阻塞了。

redux-sage 提供了一个叫 fork 的 Effect,可以实现非阻塞的方式,下面我们重新设计上面的登录例子:

function* authorize(user, password) {
try {
const token = yield call(Api.authorize, user, password)
yield put({type: 'LOGIN_SUCCESS', token})
} catch(error) {
yield put({type: 'LOGIN_ERROR', error})
}
} function* loginFlow() {
while(true) {
const {user, password} = yield take('LOGIN_REQUEST')
yield fork(authorize, user, password)
yield take(['LOGOUT', 'LOGIN_ERROR'])
yield call(Api.clearItem('token'))
}
}
  1. token 的获取放在了 authorize saga 中,因为 fork 是非阻塞的,不会返回值

  2. authorize 中的 call 和 loginFlow 中的 take 并行调用

  3. 这里 take 了两个 action , take 可以监听并发的 action ,不管哪个 action 触发都会执行 call(Api.clearItem...) 并回到 while 开始

    1. 在用户触发 logout 之前, 如果 authorize 成功,那么 loginFlow 会等待 LOGOUT action

    2. 在用户触发 logout 之前, 如果 authorize 失败,那么 loginFlow 会 take('LOGIN_ERROR')

  4. 如果在用户触发 logout 的时候,authorize 还没有执行完成,那么会执行后面的语句并回到 while 开始

这个过程中的问题是如果用户触发 logout 了,没法停止 call api.authorize , 并会触发 LOGIN_SUCCESS 或者 LOGIN_ERROR action 。

redux-saga 提供了 cancel Effect,可以 cancel 一个 fork task

import { take, put, call, fork, cancel } from 'redux-saga/effects'// ...function* loginFlow() {
while (true) {
const {user, password} = yield take('LOGIN_REQUEST')
// fork return a Task objectconst task = yield fork(authorize, user, 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, 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) {
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, '/users'),
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

function* playLevelOne() { ... }
function* playLevelTwo() { ... }
function* playLevelThree() { ... }
function* game() {
const score1 = yield* playLevelOne()
put(showScore(score1)) const score2 = yield* playLevelTwo()
put(showScore(score2)) const score3 = yield* playLevelThree()
put(showScore(score3))
}

3.5.5 channels

通过 actionChannel 实现缓存区

先看如下的例子:

import { take, fork, ... } 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, call, ... } from 'redux-saga/effects'function* watchRequests() {
// 为 REQUEST 创建一个 actionChannel 相当于一个缓冲区const requestChan = yield actionChannel('REQUEST')
while (true) {
// 重 channel 中取一个 actionconst {payload} = yield take(requestChan)
// 使用非阻塞的方式调用 requestyield 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 -= 1if (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) 会导致直接跳转到 finallylet seconds = yield take(chan)
console.log(`countdown: ${seconds}`)
}
} finally {
// 支持外部 cancel sagaif (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, fork, ... } from 'redux-saga/effects'function* watchRequests() {
// 创建一个空的 channelconst chan = yield call(channel)
// fork 3 个 worker sagafor (var i = 0; i < 3; i++) {
yield fork(handleRequest, chan)
}
while (true) {
// 等待 request actionconst {payload} = yield take('REQUEST')
// put payload 到 channel 中yield put(chan, payload)
}
} function* handleRequest(chan) {
while (true) {
const payload = yield take(chan)
// process the request
}
}

3.5 compose redux sages的更多相关文章

  1. Redux源码分析之compose

    Redux源码分析之基本概念 Redux源码分析之createStore Redux源码分析之bindActionCreators Redux源码分析之combineReducers Redux源码分 ...

  2. Redux源码分析之createStore

    接着前面的,我们继续,打开createStore.js, 直接看最后, createStore返回的就是一个带着5个方法的对象. return { dispatch, subscribe, getSt ...

  3. Redux源码分析之applyMiddleware

    Redux源码分析之基本概念 Redux源码分析之createStore Redux源码分析之bindActionCreators Redux源码分析之combineReducers Redux源码分 ...

  4. Redux源码分析之基本概念

    Redux源码分析之基本概念 Redux源码分析之createStore Redux源码分析之bindActionCreators Redux源码分析之combineReducers Redux源码分 ...

  5. Redux源码分析之bindActionCreators

    Redux源码分析之基本概念 Redux源码分析之createStore Redux源码分析之bindActionCreators Redux源码分析之combineReducers Redux源码分 ...

  6. Redux源码分析之combineReducers

    Redux源码分析之基本概念 Redux源码分析之createStore Redux源码分析之bindActionCreators Redux源码分析之combineReducers Redux源码分 ...

  7. react系列笔记:第一记-redux

    前言: 目前公司使用dva,对于前不久还是使用原生js的我来说,花了差不多一两周时间,基本掌握如何使用.虽然对于react有一点点基础,但很多地方未深入,很多概念也很模糊,故从本文开始,记录一下系统的 ...

  8. react分享

    后台项目应用分享 后台项目应用分享 webpack + react + redux + antd 后台项目应用分享 策略篇 框架选择 组件化开发 组件?组件! CSS in JS下的样式开发思路 展示 ...

  9. 从 源码 谈谈 redux compose

    compose,英文意思 组成,构成. 它的作用也是通过一系列的骚操作,实现任意的.多种的.不同的功能模块的组合,用来加强组件. 看看源码 https://github.com/reactjs/red ...

随机推荐

  1. 2019牛客第八场多校 E_Explorer 可撤销并查集(栈)+线段树

    目录 题意: 分析: @(2019牛客暑期多校训练营(第八场)E_Explorer) 题意: 链接 题目类似:CF366D,Gym101652T 本题给你\(n(100000)\)个点\(m(1000 ...

  2. PAT 1051 Pop Sequence (25 分)

    返回 1051 Pop Sequence (25 分)   Given a stack which can keep M numbers at most. Push N numbers in the ...

  3. [转]DrawPrimitive 详解Direct3DDevice8

    Direct3DDevice8 函数 05-39  DrawPrimitive 详解 费了好大的劲,终于搞清楚 DirectX 3D 三维图像中 DrawPrimitive 的用法(自嘲:未必). D ...

  4. CSS3 resize 属性

    CSS3 resize 属性 CSS 参考手册 实例 规定可以由用户调整 div 元素的大小: div { resize:both; overflow:auto; } 支持 Firefox 4+.Ch ...

  5. js添加onclick中自定义方法

    最近写一个插件的时候遇到了这么一个问题. 插件的要求是,仅仅通过一行js代码,就需要生成相应的页面,不能改变源文件的代码 生成页面还好说,但是有一个问题就是,生成的页面中是有一个按钮的.按钮也是可以添 ...

  6. 20140919 进程间通信 系统栈 用户栈 多级反馈队列 windows 内存管理

    1.进程间通信 共享内存(剪切板) 匿名管道只能实现父子进程间的通信(以文件系统为基础): 匿名管道是什么,有什么用,怎么用 1.创建父进程,也就是在解决方案中建立一个parent的工程 2.在par ...

  7. 条件选择case

    SELECT COUNT(*),count(CASE b.AUTHORITY WHEN 'addAsmAccessControlList' THEN '1' ELSE NULL END) as aut ...

  8. js数组去重练习

  9. 虚拟机linux系统 硬盘/root路径扩容

    调整完后,重新打开虚拟机,使用fdisk -l查看,可以看到我们刚刚扩容的空间已经可以看到,但没有分区,还不能使用./dev/sda已经拥有了扩大的空间. 使用Linux的fdisk分区工具给磁盘/d ...

  10. [转载]Ubuntu Server下配置UTF-8中文环境

    转载自:http://www.gaojinbo.com/ubuntu-server%E4%B8%8B%E9%85%8D%E7%BD%AEutf-8%E4%B8%AD%E6%96%87%E7%8E%AF ...