之前研究过了JavaScript中是怎么处理异步的操作的,现在我们再来看看放到一个具体的框架中,该如何应对异步的操作。这里主要探讨Redux框架下一个React应用中的异步请求操作。

原生React中的异步操作##

在讲Redux之前,先看一看原生的React app中是如何处理异步的操作的。

直接上例子:

var RepoList = React.createClass({
  getInitialState: function() {
    return {
      loading: true,
      error: null,
      data: null
    };
  },

  componentDidMount() {
    this.props.promise.then(
      value => this.setState({loading: false, data: value}),
      error => this.setState({loading: false, error: error}));
  },

  render: function() {
    if (this.state.loading) {
      return <span>Loading...</span>;
    }
    else if (this.state.error !== null) {
      return <span>Error: {this.state.error.message}</span>;
    }
    else {
      var repos = this.state.data.items;
      var repoList = repos.map(function (repo, index) {
        return (
          <li key={index}><a href={repo.html_url}>{repo.name}</a> ({repo.stargazers_count} stars) <br/> {repo.description}</li>
        );
      });
      return (
        <main>
          <h1>Most Popular JavaScript Projects in Github</h1>
          <ol>{repoList}</ol>
        </main>
      );
    }
  }
});

ReactDOM.render(
  <RepoList promise={$.getJSON('https://api.github.com/search/repositories?q=javascript&sort=stars')} />,
  document.getElementById('example')
);

这是react-demos中的一个例子,该组件会发送一个请求来获取Github上面JavaScript类别的热门Repo。这个请求操作就是一个异步的操作,可以看到这里是直接将一个Promise对象传递到了组件的promise属性中,在组件加载好后通过调用this.props.promise.then来定义请求成功后的操作:将请求的结果更新到组件的状态中,从而可以重新渲染组件来展现请求结果。

简单分析下,可以得到这样一个模式来处理异步的操作:

将回调函数接口传递给组件 => 组件加载时定义回调函数,该回调函数会更新组件状态 => 回调函数在异步操作结束时被调用 => 组件根据状态的改变来重新渲染

Redux中的异步操作##

其实在原生的Redux就可以完成上面一模一样的操作,只不过流程上会稍微有些变化:

将回调函数接口传递给组件 => 组件加载时定义回调函数,该回调函数会dispatch一个action并携带请求返回的内容 => reducer函数根据action类型来更新状态,进而映射到组件的属性 => 回调函数在异步操作结束时被调用 => 组件根据属性的改变来重新渲染

以上,可以总结出:完成异步操作的关键就在于,在异步操作的回调函数中去改变组件(或Redux)的状态。

具体的代码就不列出来的,需要的可以看这里

Redux中的异步操作链##

OK,现在我们将问题变得更加复杂一些,也更加贴近实际:我想在第一个请求成功以后再发送第二个请求,再以此更新相应的组件内容。

如果还要在原生的Redux上完成这件事情,那么大概需要在mapDispatchToProps里面进行Promise的串联,就像下面这样(因为每个操作都需要dispatch action,所以只能在mapDispatchToProps里面做这件事情):

const mapDispatchToProps = (dispatch, ownProps) => {
    return {
        handlePromise: () => {
            ownProps.promise.then(
                value => {
                    dispatch({type: 'UPDATE_DATA', data: value.data});
                    return axios.get('https://api.github.com/search/repositories?q=python&sort=stars');
                },
                error => {
                    dispatch({type: 'UPDATE_ERROR', error: error});
                }
            ).then(
                value => {
                    dispatch({type: 'UPDATE_DATA', data: value.data});
                },
                error => {
                    dispatch({type: 'UPDATE_ERROR', error: error});
                }
            );
        },
    }
};

或者直接使用async/await:

async function asyncCall(dispatch, ownProps) {
    try {
        const jsData = await ownProps.promise;
        dispatch({type: 'UPDATE_DATA', data: jsData.data});
    }
    catch (error) {
        dispatch({type: 'UPDATE_ERROR', error: error});
    }
    try {
        const pyData = await axios.get('https://api.github.com/search/repositories?q=python&sort=stars');
        dispatch({type: 'UPDATE_DATA', data: pyData.data});
    }
    catch (error) {
        dispatch({type: 'UPDATE_ERROR', error: error});
    }
}


const mapDispatchToProps = (dispatch, ownProps) => {
    return {
        handlePromise: () => {
            asyncCall(dispatch, ownProps);
        },
    }
};

自从有了async函数,妈妈再也不用担心我的异步流程的操作~

使用redux-thunk###

但是,不要高兴得太早!注意到上面的代码最大的限制是什么了吗?那就是所有的异步操作都必须在mapDispatchToProps里面实现,因为只有它提供了我们dispatch函数。这是原生的react-redux施加的限制。

但往往我们的应用会需要在进行操作的同时获取/修改Redux状态,比如有这样的需求:在发送一个请求之前会先验证下所填的表单是否符合要求,如果符合要求则发送请求,否则在页面中提示用户修改表单。简单分析下,这个需求下在用户点击了提交按钮后会有这么几个action:验证表单(可能会改变Redux状态)、发送请求(需要获取Redux状态)。修改Redux状态可以通过让action夹带数据来实现,但获取Redux状态在mapDispatchToProps里就没办法了。

为此,一种解决办法是使用redux-thunk中间件。redux-thunk带来的一个好处就是我们可以在action里面同时拥有dispatch函数和getState函数,后者正是用来获取整个Redux状态的函数。

最后实现起来大概是这个样子[完整代码]:

function handleInputChange(event) {
    return async function asyncCall(dispatch, getState) {
        dispatch({type: 'INPUT_CHANGE', value: event.target.value});
        const state = getState();
        if (state.validInput) {
            try {
                const response = await axios.get(`https://api.github.com/search/repositories?q=${state.input}&sort=stars`);
                dispatch({type: 'UPDATE_DATA', data: response.data});
            }
            catch (error) {
                dispatch({type: 'UPDATE_ERROR', error: error});
            }
        }
    }
}

const mapDispatchToProps = (dispatch, ownProps) => {
    return {
        handleInputChange: (event) => {
            dispatch(handleInputChange(event));
        }
    }
};

注意dispatch操作是同步的操作,所以这里可以直接串联起来多个dispatch操作。

使用redux-saga

另一种更加终极的解决方法是使用redux-saga这个更加强大的中间件。redux-saga使用了生成器的方式来处理异步操作,实际掌握起来和async函数差不多。使用redux-saga的好处在于,所有的action都是普通JavaScript对象,而不像用了redux-thunk后,有些action实际是thunk函数,这一点带来的直接的好处是action测试起来更加容易了;并且由于所有异步的操作都集中放到一起了,使得整体的代码更加清晰。

上面的需求使用redux-saga实现后,mapDispatchToProps部分就很简洁了[完整代码]:

const mapDispatchToProps = (dispatch, ownProps) => {
    return {
        initPage: () => {
            dispatch({type: 'SAGA_GET_BACKEND'});
        },

        handleInputChange: (event) => {
            dispatch({type: 'SAGA_INPUT_CHANGE', value: event.target.value});
        }
    }
};

sagas.js中,我们再对这两个新的action进行处理(注意这里这里只处理异步操作流程,至于每个操作实际对Redux状态产生影响的部分还是交由reducer函数来处理):

import {put, takeEvery, select, call} from 'redux-saga/effects'
import axios from 'axios'


function* getHottestRepoAsync(action) {
    const state = yield select();
    try {
        const response = yield call(axios.get, `https://api.github.com/search/repositories?q=${state.input}&sort=stars`);
        yield put({type: 'UPDATE_DATA', data: response.data});
    }
    catch (error) {
        yield put({type: 'UPDATE_ERROR', error: error});
    }
}

function* handleInputChange(action) {
    yield put({type: 'INPUT_CHANGE', value: action.value});
    const state = yield select();
    if (state.validInput) {
        yield put({type: 'SAGA_GET_BACKEND'});
    }
}

function* watchAsync() {
    yield takeEvery('SAGA_GET_BACKEND', getHottestRepoAsync);
    yield takeEvery('SAGA_INPUT_CHANGE', handleInputChange);
}

export default function* rootSaga() {
    yield [
        watchAsync(),
    ]
}

使用redux-observable###

redux-observable是另外一个Redux中间件(Redux全家桶还真是丰富啊~),它将RxJS应用到了Redux状态处理上,结果达到了非常不错的效果。该中间件的核心思想在于实现一个函数,Actions in, actions out,从而串联起异步的操作(其实有点类似redux-saga啊,都是监听某个action然后再做一些异步的操作,当然这些操作是用RxJS来做了)。

具体的例子以后有时间再放吧。。。

小结###

JavaScript中对于异步操作的处理一直是一个大命题,即使在各种框架下,对异步操作的处理也能单独拿出来研究形成一个lib。Promise以及async函数的出现很大程度上缓解了异步操作之痛,可以看到即使是在原生的Redux框架下(最多加上redux-thunk),使用async函数已经可以很方便地处理/协同多个异步操作了。