处理Redux框架下的异步操作
之前研究过了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函数已经可以很方便地处理/协同多个异步操作了。
Comments