基于 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. mysql 用户权限管理详细

    用户管理 mysql>use mysql; 查看 mysql> select host,user,password from user ; 创建 mysql> create user ...

  2. oracle 中和mysql的group_concat有同样作用的写法

    所有版本的oracle都可以使用select wm_concat(name) as name from user;但如果是oracle11g,使用select listagg(name, ',') w ...

  3. Windows 08 R2_NLB负载均衡(图文详解)

    目录 目录 Load Balance 使用NLB来部署Web Farm集群 环境准备 在Win08r2pc1中配置DNS服务 在Win08r2pc1中部署File Service文件服务 在Win08 ...

  4. Linux命令 who

    who :显示当前登入系统的用户信息 显示的内容主要包括: 用户名,登录终端,上线时间,停留时间,动作,UID等 权限:所有使用者 语法: who  [option] ...[ file | arg1 ...

  5. Findwind() Enumwindow()

    1. FindWindow() HWND FindWindow(LPCSTR lpClassName, LPCSTR lpWindowName); 功能:查找与指定窗口类名和窗口名称相匹配的顶级窗口, ...

  6. UML指南系列——用例图

    可以用用例来描述正在开发的系统想要实现的行为,而不必说明这些行为如何实现. 结构良好的用例只表示系统或者子系统的基本行为,而且既不过于笼统也不过于详细.

  7. zabbix-Graphs

    图形 概述 随着大量的监控数据被采集到Zabbix中,如果用户可以以可视化的表现形式来查看发生了什么事情,那么和仅仅只有数字的表现形式比起来则更加轻松. 以下是进行图形设置的地方.图形可以一目了然地掌 ...

  8. 怎么在vue-cli中利用 :class去做一个底层背景或者文字的点击切换

    // html <div class="pusherLists" :class="{hidden: isHidden}"> <span @cl ...

  9. spring security 学习二

    doc:https://docs.spring.io/spring-security/site/docs/ 基于表单的认证(个性化认证流程): 一.自定义登录页面 1.在securityConfigy ...

  10. 一、hibernate环境搭建

    hibernate环境搭建 下载hibernate hibernate的jar 连接数据库的jar 解压hibernate,解压后目录结构 documentation :对应hibernate开发文档 ...