在react+redux的技术栈中优雅的使用异步

在使用react和redux进行前端开发时,一定会遇到异步的action处理,这个使用redux-thunk可以很好地处理,相信你已经知道了。但对于多个异步请求级联触发的情况,怎么处理才好呢?

本文使用一个实际的例子就这个问题进行一些探讨。相关代码都在这个代码库中:https://github.com/cui-liqiang/react-redux-async-chain-example

阅读本文,假设你已经理解了以下知识:

  1. react和redux的基本使用
  2. redux store的中间件机制及redux-thunk中间件
  3. co和generator
  4. async/await
  5. promise

基础代码

下载示例代码,切换到base分支。并在根目录中启动web server之后。访问http://localhost:8000/index.html ,便可看到下面的图片:
在react+redux的技术栈中优雅的使用异步

其中左边是模拟的后台数据。右边是可以进行的操作。在这个分支上可以看到两个操作:

  1. 按照名字加载用户
  2. 按照用户ID加载Post

这两个接口背后是通过setTimeout模拟出来的(fakeRemote.jsx)

fetchUser(name)
fetchPostsByUser(userId)

模拟的异步接口是回调风格的,并使用redux-chunk中间件来处理异步action。见action.jsx:

function fetchUserAction(name) {
  return dispatch => {
    return fetchUser({
      name
    }, (result) => {
      return dispatch(setUser(result));
    })
  }
}

要处理的问题

现在已经有了两个操作,我们要实现第三个操作:”按用户名加载用户信息及Post“。也就是上面两个操作的组合。但由于这两个操作都是异步的,因此最简单的组合方式就是新写一个action,叫做“fetchUserAndRelatedPost”,使用回调套回调的方式实现。这种方式有两个问题:

  1. 回调套回调太恶心(曾经忍着恶心写了4层的异步回调,写到最后,各种括号都对不上了)
  2. 多个action之间也出现了重复

因此期望的方式是能够在react组件中复用已有的这两个action,进行优雅的组合。本代码库使用了两种方式进行了实现。

使用async/await

切换到async分支看最终代码。

async/await是一种化异步为同步的编码方式,具体原理不在这里展开。简单讲一下代码。

首先,能够await的东西需要是一个promise。什么是promise呢,就是能够响应then和catch这两函数的一个对象。所以需要把之前异步action中的回调风格改成promise风格。

//这里加了1的后缀,是因为没有使用后端预处理,所有函数都在顶层作用域,和后面要将的co中的一个方法重名了,所以要区分下。
function toPromise1(f) {
  return (option) => {
    return new Promise((resolve, reject) => {
      f(option, resolve)
    })
  }
}

然后把前面的回调风格的action改成promise风格:

function fetchUserAction(name) {
  return dispatch => {
    return toPromise1(fetchUser)({
      name
    }).then((result) => {
      return dispatch(setUser(result));
    })
  }
}

这里需要强调的一点,这些Action结尾的函数,其实并不是Action,而是“ActionCreator”,调用它返回的那个值才是action,因为太长了,所以都懒得写全。经过react-redux的connect之后,在react组件中调用this.props.fetchUserAction(userName),就相当于调用store.dispatch(fetchUserAction(userName))。由于这个action是个函数,且我们使用了redux-thunk这个中间件,所以这个调用最终的返回值其实是下面这一坨东西。

return toPromise1(fetchUser)({
  name
}).then((result) => {
  return dispatch(setUser(result));
})

也就是一个promise,上面提到了只有promise才能被await,所以才能在react组件中使用这样的代码:await this.props.fetchUserAction(this.state.name)

然后还需要把action中返回的函数使用async进行修饰,这样才能在react组件中使用await进行等待:

// action定义
function fetchUserAction(name) {
  return async dispatch => {
    return toPromise1(fetchUser)({
      name
    }).then((result) => {
      return dispatch(setUser(result));
    })
  }
}

// 组件中dispatch的调用(container.jsx)
// 由于await只能在async标记的函数中使用,所以这里也加上了async修饰
async fetchUserAndPost() {
  await this.props.fetchUserAction(this.state.name)
  await this.props.fetchPostsByUserAction(this.props.user.id)
}

这样就可以在不增加新的action的前提下,在组件中通过组合来完成自己特性的业务诉求。

使用co+generator

切换到co分支看代码。

co+generator也是化异步为同步的神器。效果上基本和async/await类似,写法略有不同。

相同之处是,都需要action(不是action creator哦)返回的结果是一个promise。不同之处是,返回promise的那个函数不需要使用async修饰。

await只能在async的函数中使用,本方案中与await对应的是yield,它只能在generator中使用。并且需要使用co这个函数来驱动generator的执行。因此写出来是这样的:

// action定义,不需要async修饰
function fetchUserAction(name) {
  return dispatch => {
    return toPromise1(fetchUser)({
      name
    }).then((result) => {
      return dispatch(setUser(result));
    })
  }
}

// 使用co驱动一个generator,在generator内部使用yield表示等待(container.jsx)
fetchUserAndPost() {
  const that = this;
  co(function* () {
    yield that.props.fetchUserAction(that.state.name)
    that.props.fetchPostsByUserAction(that.props.user.id)
  })
}

可以看出,async方案和co+generator都能达到不错的效果。且原理非常的相似,简单的总结下:

  1. 串行化代码的上下文,async/await使用关键字驱动;co+generator还需要使用co这个函数包一层来驱动,这点上比async/await差一点。
  2. 等待方式,一个用await,一个用yield,都是在等待一个promise的完成。
  3. 都需要作用于promise。

最后强调一下,上面的写法本质都是语法糖,实际的执行还都是异步的,只是编写起来更加符合直觉,编写出来的代码更加易于理解和维护。

其他方案

上面的两个方案其实已经足够优雅了,但如果你对无状态有非常高的追求,还可以尝试使用redux-saga来进一步隔离无副作用和有副作用的代码。它也是使用generator来实现的,可以理解为它把上述方案中的co部分的工作做掉了,并且让react组件中的代码“看起来”完全无副作用。相信理解上上述的做法,理解redux-saga就很容易了。

上一篇:MySQL关闭Enterprise Server源码


下一篇:CRP中多分支持续集成的配置