Redux 异步中间件

上篇博客已经提到了 redux-thunk, redux-promise 和 redux-saga 这三个库. 他们加上 redux-promise-middleware 都是 Redux 异步 Action 的常用的解决方案. 本文就结合其源码来分析他们的异同和优缺点.

redux-thunk

redux-thunk 是 Redux 作者 Dan 写的中间件. 通过使用该中间件, Redux 支持 dispatch 一个函数(actionCreator), 函数内部可以继续 dispatch.

redux-thunk 的源码关键的就这几行:

function createThunkMiddleware(extraArgument) {
  return ({ dispatch, getState }) => next => action => {
    if (typeof action === 'function') {
      return action(dispatch, getState, extraArgument)
    }
    return next(action)
  }
}

需要注意的是, redux-thunk 依赖的 redux 版本比较旧了. 与最新的 redux 不兼容, 所以要结合其依赖的 redux 版本的 applyMiddleware 来看

componentWillMount () {
  store.dispatch(fetchBookList(1))
}
function fetchBookList(page) {
  return async function (dispatch, getState) {
    try {
      // 准备获取数据, 比如设置加载中动画, 或者进行预更新
      dispatch({ type: 'GET_DATA_PRE' })
      const data = await axios.get(`/book/${page}`)
      // 设置数据
      dispatch({ type: 'GET_DATA_SUCCESS', payload: data }) 
    } catch (e) {
      // 数据获取失败处理, 可以撤销预更新处理
      dispatch({ type: 'GET_DATA_ERROR', payload: e.message }) 
    } finally {
      // 完成获取, 可以结束加载中动画
      dispatch({ type: 'GET_DATA_AFTER' }) 
    }
  }
}

上篇博客已经提到了 redux-thunk 的其中一个问题就是, store.dispatch 方法本身仍然是同步的, 另外, fetchBookList 方法内部诸如 GET_DATA_PRE, GET_DATA, GET_DATA_SUCCESS, GET_DATA_ERROR, GET_DATA_AFTER 这些 action 也有点啰嗦(但我们需要异步调用前后以及成功与否的情况).

不过对于简单项目来说, redux-thunk 还是可以用的.

redux-promise

我们先看下 redux-promise 的源码:

export default function promiseMiddleware({ dispatch }) {
  return next => action => {
    if (!isFSA(action)) {
      return isPromise(action)
        ? action.then(dispatch)
        : next(action);
    }

    return isPromise(action.payload)
      ? action.payload.then(
          result => dispatch({ ...action, payload: result }),
          error => {
            dispatch({ ...action, payload: error, error: true });
            return Promise.reject(error);
          }
        )
      : next(action);
  };
}

代码并不难理解, 不过需要注意的是, 和 redux-thunk 一样, redux-promise 和 redux-thunk 所依赖的 redux 版本都是比较旧的, 在这里, next 即原生 store.dispatch, 而 dispatch 则为 (...args) => store.dispatch(...args). 之所以用 dispatch 应该是为了避免原生 store.dispatch 被修改.

store.dispatch 经过这个中间件处理后, 应该以以下几种方式调用:

  1. 传入 FSA, payload 不是 Promise

    中间件不能破坏原有的行为, 这里判断 action.payload 是否为 Promise, 如果不是, 则直接 next(action).

    所以 next(action), 即相当于未使用中间件的 store.dispatch(action).

  2. 传入 FSA, 但 payload 为 Promise

    此时则会等待 payload 的 Promise resolved/rejected 后进行相应的 dispatch 原 Action, 但 payload 改为 Promise resolved/rejected 之后的结果, 如果 Promise 是 rejected 的话, 还会额外传入 { error: true }.

    这里需要特别一提, redux-promise 会自动的在失败的情况下 dispatch 一个 Action. 意味着我们可以不需要手动 catch Promise 的错误然后去 dispatch 事件处理. 我们在针对该 action.type 的处理中, 可以判断action.error 是否为 true 从而进行相应的处理. 这相对于 redux-thunk 一定程度上减少了代码量.

  3. 传入 Promise

    除了接收标准 Flex Action Object 之外, 也可以传入一个 Promise. 该情况下, 会 dispatch Promise resolved 之后的结果. 该情况不像第二种情况, 不会对 rejected 进行处理. Promise 的结果应该是返回一个 FSA.

但 redux-promise 并不是就完美了, 它也存在问题, 即无法处理乐观更新.

对于大多数场景, 我们一般都是先发送请求, 请求成功才更新视图, 特别是我们并不知道确切的数据是什么的时候. 但也有一些场景, 我们可以预先知道请求成功之后的结果, 所以可以先更新视图, 同时发送请求, 请求失败再重新更新下视图.

后者常见于即时通讯中, 一半聊天我们发送消息, 消息都是先渲染出来了, 同时请求也发出, 如果请求失败, 则会更新视图说明该消息发送失败. 这就是乐观更新的情况.

在 redux-thunk 中 , GET_DATA_PRE 中我们可以进行乐观更新操作. 而在 redux-promise 中, 我们虽然可以传 Promise 给 payload, 但是并无法在 Promise 中进行 dispatch 操作, 因此也就无法进行乐观更新(除非你自己在 dispatch 前单独 dispatch 一个乐观更新, 但明显很不可行).

redux-promise-middleware

这个和 redux-promise 类似. 但解决了乐观更新的问题. 官网介绍就是:

Redux middleware for resolving and rejecting promises with conditional optimistic updates

redux-promise-middleware 的源码相比上面两个稍微长了一些, 相比 redux-promise 多做了一些处理. 但也很容易理解. redux-promise-middleware 会自动触发至少两个 Action. 分别是:

  • ${type}__PENDING

    这个 Action 就是用于进行乐观更新操作的了, 此时 payload 为 pending 状态的 Promise, 所以实际上 payload 没什么卵用.

  • ${type}__FULFILLED / ${type}__REJECTED

    如果 Promise resolved 的话则会触发前面的 Action, 否则触发后面的 Action. Action 的结构和 redux-promise 相同.

值得一提的是, 它本身返回了一个 Promise, 该 Promise 在 resolved/rejected 会返回如下结果

// Rejected object:
{
  reason: 'xxx',
  action: {     
    error: true,
    type: 'ACTION_REJECTED',
    payload: 'xxx'
  }
}
// Fullfilled object:
{
  value: 'xxx',
  action: {
    type: 'ACTION_FULFILLED',
    payload: 'xxx'
  }
}

返回结果和 redux-promise 有差异. redux-promise 在 resolved 时返回原 Action(payload 变为 resolved 的结果), 在 rejected 时直接返回的错误信息.

相比较起来, 我觉得 redux-promise-middleware 在返回值上面处理比较好.

redux-promise-middleware 对 action 层进行了大大的简化, 我们不需要像 redux-thunk 那样写大量的 dispatch 代码.

但很遗憾的是, 以上的三个方案在 reducer 的处理上并没有任何改善.

redux-saga

redux-saga 是这四个库中最出名的一个. 它让异步行为成为架构中独立的一层(saga), 既不在 ActionCreator 中, 也不和 reducer 沾边. 这个库相当强大, API 也很多, 所以源码自然也比较多了.

// todo
redux-saga 的代码比较多,用法也比较复杂,还没细看。暂时不打算花时间去看。博客上个月就写到这里了,还是先发出来吧。后面再补充完善。