redux-saga学习进阶篇一

今日学习目录

一、错误处理try/catch

二、 take/takeEvery 监听未来的action

三、无阻塞调用 fork、canceled

四、同时执行多个任务

 

一、错误处理try/catch

我们可以使用熟悉的 try/catch 语法在 Saga 中捕获错误。

import Api from './path/to/api'
import { call, put } from 'redux-saga/effects'

// ...

function* fetchProducts() {
  try {
    const products = yield call(Api.fetch, '/products')
    yield put({ type: 'PRODUCTS_RECEIVED', products })
  }
  catch(error) {
    yield put({ type: 'PRODUCTS_REQUEST_FAILED', error })
  }
}

为了测试故障案例,我们将使用 Generator 的 throw 方法。

// 在这个案例中,我们传递一个模拟的 error 对象给 throw,
// 这会引发 Generator 中断当前的执行流并执行捕获区块(catch block)。
import { call, put } from 'redux-saga/effects'
import Api from '...'

const iterator = fetchProducts()

// 期望一个 call 指令
assert.deepEqual(
  iterator.next().value,
  call(Api.fetch, '/products'),
  "fetchProducts should yield an Effect call(Api.fetch, './products')"
)

// 创建一个模拟的 error 对象
const error = {}

// 期望一个 dispatch 指令
assert.deepEqual(
  iterator.throw(error).value,
  put({ type: 'PRODUCTS_REQUEST_FAILED', error }),
  "fetchProducts should yield an Effect put({ type: 'PRODUCTS_REQUEST_FAILED', error })"
)

当然了,你并不一定得在 try/catch 区块中处理错误,你也可以让你的 API 服务返回一个正常的含有错误标识的值。例如, 你可以捕捉 Promise 的拒绝操作,并将它们映射到一个错误字段对象。

import Api from './path/to/api'
import { call, put } from 'redux-saga/effects'

function fetchProductsApi() {
  return Api.fetch('/products')
    .then(response => ({ response }))
    .catch(error => ({ error }))
}

function* fetchProducts() {
  const { response, error } = yield call(fetchProductsApi)
  if (response)
    yield put({ type: 'PRODUCTS_RECEIVED', products: response })
  else
    yield

二、 take/takeEvery 监听未来的action

// 1. 使用 takeEvery('*')(使用通配符 * 模式),我们就能捕获发起的所有类型的 action。
import { select, takeEvery } from 'redux-saga/effects'

function* watchAndLog() {
  yield takeEvery('*', function* logger(action) {
    const state = yield select()

    console.log('action', action)
    console.log('state after', state)
  })
}

// 2. 使用 take Effect 来实现和上面相同的功能:

import { select, take } 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)
  }
}

take 就像我们更早之前看到的 call 和 put。它创建另一个命令对象,告诉 middleware 等待一个特定的 action。 正如在 call Effect 的情况中,middleware 会暂停 Generator,直到返回的 Promise 被 resolve。 在 take 的情况中,它将会暂停 Generator 直到一个匹配的 action 被发起了。 在以上的例子中,watchAndLog 处于暂停状态,直到任意的一个 action 被发起。

使用 take 组织代码有一个小问题。在 takeEvery 的情况中,被调用的任务无法控制何时被调用, 它们将在每次 action 被匹配时一遍又一遍地被调用。并且它们也无法控制何时停止监听。

// 主动拉取 action 的另一个好处是我们可以使用熟悉的同步风格来描述我们的控制流。

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

take Effect 让我们可以在一个集中的地方更好地去描述一个非常规的流程。

三、无阻塞调用 fork、canceled

在上面这个例子中,使用已拥有的effects实现上述

// 下面代码存在一个小问题
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 在 authorize 中被阻塞了,最终发生在开始调用和收到响应之间的 LOGOUT 将会被错过, 因为那时 loginFlow 还没有执行 yield take('LOGOUT')。

所以为了让 loginFlow 不错过一个并发的 LOGOUT,我们不应该使用 call 调用 authorize 任务,而应该使用 fork。并且,自从 authorize 的 action 在后台启动之后,我们获取不到 token 的结果(因为我们不应该等待它)。 所以我们需要将 token 存储操作移到 authorize 任务内部。

import { fork, call, take, 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})
  } 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'))
  }
}

但是还没完。

如果我们在 API 调用期间收到一个 LOGOUT action,我们必须要 取消 authorize 处理进程,否则将有 2 个并发的任务, 并且 authorize 任务将会继续运行,并在成功的响应(或失败的响应)返回后发起一个 LOGIN_SUCCESS action(或一个 LOGIN_ERROR action),而这将导致状态不一致。

// 为了取消 fork 任务,我们可以使用一个指定的 Effect cancel。
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'))
  }
}

假设在我们收到一个 LOGIN_REQUEST action 时,我们在 reducer 中设置了一些 isLoginPending 标识为 true,以便可以在界面上显示一些消息或者旋转 loading。 如果此时我们在 Api 调用期间收到一个 LOGOUTaction,并通过 杀死它(即任务被立即停止)简单粗暴地中止任务。 那我们可能又以不一致的状态结束了。因为 isLoginPending 仍然是 true,而 reducer 还在等待一个结果 action(LOGIN_SUCCESS 或 LOGIN_ERROR)。

// 幸运的是,cancel Effect 不会粗暴地结束我们的 authorize 任务,
// 相反它会给予一个机会执行清理的逻辑。
import { take, call, put, cancelled } 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})
    yield call(Api.storeItem, {token})
    return token
  } catch(error) {
    yield put({type: 'LOGIN_ERROR', error})
  } finally {
    if (yield cancelled()) {
      // ... put special cancellation handling code here
    }
  }
}

四、同时执行多个任务

// 错误写法,effects 将按照顺序执行
const users = yield call(fetch, '/users'),
      repos = yield call(fetch, '/repos')

// 由于第二个 effect 将会在第一个 call 执行完毕才开始。所以我们需要这样写:
import { call } from 'redux-saga/effects'

// 正确写法, effects 将会同步执行
const [users, repos] = yield [
  call(fetch, '/users'),
  call(fetch, '/repos')
]

posted @ 2019-07-04 20:18  林璡  阅读(706)  评论(0编辑  收藏  举报