资讯专栏INFORMATION COLUMN

精益 React 学习指南 (Lean React)- 3.5 compose redux saga

Joyven / 2229人阅读

摘要:通过可以实现很多有趣的简洁的控制。这里默认使用到了的一个特性,如果某一个任务成功了过后,其他任务都会被。组合是的内关键字,使用的场景是一个。

书籍完整目录

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", 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 logic
    yield 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 的流程:

通过 take effect 监听 login_request action

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

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

等待 logout action

logout 事件触发后异步清除 token

然后回到第 0 步

失败(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"))
  }
}

token 的获取放在了 authorize saga 中,因为 fork 是非阻塞的,不会返回值

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

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

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

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

如果在用户触发 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 object
    const 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 中取一个 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, fork, ... } 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
  }
}
参考链接

http://yelouafi.github.io/redux-saga/docs/advanced/index.html

http://gajus.com/blog/2/the-definitive-guide-to-the-javascript-generators#understanding-the-execution-flow

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/79753.html

相关文章

  • 精益 React 学习指南Lean React)- 3.4 掌控 redux 异步

    摘要:举例来说一个异步的请求场景,可以如下实现任何异步的逻辑都可以,如等等也可以使用的和。实际上在中,一个就是一个函数。 书籍完整目录 3.4 redux 异步 showImg(https://segmentfault.com/img/bVyou8); 在大多数的前端业务场景中,需要和后端产生异步交互,在本节中,将详细讲解 redux 中的异步方案以及一些异步第三方组件,内容有: redu...

    JouyPub 评论0 收藏0
  • 精益 React 学习指南Lean React)- 3.3 理解 redux 中间件

    摘要:数组为新的数组,包含了方法将新的和结合起来,生成一个新的方法返回的新增了一个方法,这个新的方法是改装过的,也就是封装了中间件的执行。 书籍完整目录 3.3 理解 Redux 中间件 showImg(https://segmentfault.com/img/bVymkt); 这一小节会讲解 redux 中间件的原理,为下一节讲解 redux 异步 action 做铺垫,主要内容为: ...

    Kerr1Gan 评论0 收藏0
  • 精益 React 学习指南Lean React)- 4.2 react patterns

    摘要:另外一点是组件应该尽量保证独立性,避免和外部的耦合,使用全局事件造成了和外部事件的耦合。明确的职责分配也增加了应用的确定性明确只有组件能够知道状态数据,且是对应部分的数据。 书籍完整目录 4.2 react patterns 修改 Props Immutable data representation 确定性 在 getInitialState 中使用 props 私有状态和...

    Berwin 评论0 收藏0
  • 精益 React 学习指南Lean React)- 1.1 React 介绍

    摘要:单向数据流应用的核心设计模式,数据流向自顶向下我也是性子急的人,按照技术界的惯例,在学习一个技术前,首先得说一句。然而的单向数据流的设计让前端定位变得简单,页面的和数据的对应是唯一的我们可以通过定位数据变化就可以定位页面展现问题。 书籍完整目录 1.1 React 介绍 showImg(https://segmentfault.com/img/bVvJgS); 1.1.1 React ...

    lsxiao 评论0 收藏0

发表评论

0条评论

Joyven

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<