【React全家桶入门之十三】Redux中间件与异步action

时间:2022-08-31 20:00:51

在上一篇中我们了解到,更新Redux中状态的流程是这样的:action -> reducer -> new state。

文中也讲到,action是一个普通的javascript对象、reducer是一个普通的方法,在reducer中根据当前的state、接收到的action来生成一个新的state以达到更新状态的目的。

那么问题来了,每次action被触发(dispatch),reducer就会同步地对store进行更新,在实际开发项目的时候,有很多需求都是需要通过接口等形式获取异步数据后再进行更新操作的,如何异步地对store进行更新呢?

还是拿上一篇文中写的计数器来做例子,现在需要添加一个按钮,点击这个按钮的时候会在1秒之后对counter的值进行加1操作。

最简单的方法当然是把store.dispatch({type: ‘INCREASE’})放到一个setTimeout的回调里去执行:

document.getElementById('btn_async_increase').addEventListener('click', function () {
setTimeout(function () {
store.dispatch({type: 'INCREASE'});
}, 1000);
});

但是这其实还是一次同步的更新操作,并不是我们想要的。(在dispatch之后的store更新还是同步的)

使用中间件

Redux本身提供的Api与功能并不多,但是它提供了一个中间件(插件)机制,可以使用第三方提供的中间件或自己编写一个中间件来对Redux的功能进行增强,比如可以使用redux-logger这个中间件来记录action以及每次action前后的state、使用redux-undo来取消/重做action、使用redux-persist-store来对store进行持久化等等。

更多的中间件及相关工具可以查看这里

要实现异步的action,有多个现成的中间件可供选择,这里选择官方文档中使用的redux-thunk这个中间件来实现。

安装ReduxThunk

首先当然需要先获取redux-thunk,在一般的项目里使用npm i redux-thunk -S来安装到项目中,在codepen里只需简单地设置一下外部js文件引入就可以了。

使用applyMiddleware挂载中间件

在创建store的时候,我们将ReduxThunk使用Redux.applyMiddleware方法进行包装后传给Redux.createStore的第二个方法:

const {createStore, applyMiddleware} = Redux;

const store = createStore(counter, applyMiddleware(ReduxThunk.default));

异步action

原来的store.dispatch方法只能接收一个普通的action对象作为参数,当我们加入了ReduxThunk这个中间件之后,store.dispatch还可以接收一个方法作为参数,这个方法会接收到两个参数,第一个是dispatch,等同于store.dispatch,第二个是getState,等同于store.getState,也就是说,现在可以这样来触发INCREASE:

store.dispatch((dispatch, getState) => dispatch({type: 'INCREASE'}));

点击按钮一秒后执行dispatch:

<div>
<p>Count: <span id="value">0</span></p>
<button id="btn_increase">+ 1</button>
<button id="btn_async_increase">+ 1 async</button>
<button id="btn_decrease">- 1</button>
</div>
document.getElementById('btn_async_increase').addEventListener('click', function () {
store.dispatch((dispatch, getState) => {
setTimeout(() => {
dispatch({type: 'INCREASE'});
}, 1000);
});
});

看一下效果(CodePen):

ReduxThunk解析

依托于ReduxThunk我们实现了异步触发action的功能,那么ReduxThunk是怎么做到的呢?

我们来看看ReduxThunk的代码,ReduxThunk一共只有一个代码文件,14行代码:

https://github.com/gaearon/redux-thunk/blob/master/src/index.js

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

return next(action);
};
}

const thunk = createThunkMiddleware();
thunk.withExtraArgument = createThunkMiddleware;

export default thunk;

我们使用的是默认导出的thunk,也就是createThunkMiddleware()执行的结果。

createThunkMiddleware方法里返回了一个奇怪的东西:

({ dispatch, getState }) => next => action => {
...
};

换一种写法:

function ({dispatch, getState}) {
return function (next) {
return function (action) {
...
};
};
}

其实就是一个多层嵌套返回函数的函数,使用箭头的写法在函数式编程中叫做柯里化,对柯里化更详细的介绍可以看一看这篇张鑫旭的博客

第一个(最外层)方法的参数是一个包含dispatch和getState字段(方法)的对象,其实就是store对象,所以也可以写成:

store => next => action => {
...
};

这其实就是一个Redux中间件的基本写法

参数next是一个方法,这个方法的作用是通知下一个Redux中间件对这次的action进行处理: next(action),如果一个中间件中没有执行next(action),则action会停止向后续的中间件传递,并阻止reducer的执行(store将不会因为本次的action而更新)。

参数action就不用多说了,就是当前被触发的action。

在ReduxThunk这个中间件中,做的处理很简单:判断当前的action是否为一个方法,如果是,就执行action这个方法,并将store.dispatchstore.getState方法作为参数传递给action方法;如果不是,则执行next(action)将控制权转移给下一个中间件(如果有)。

所以当我们给store.dispatch方法传入一个方法的时候,ReduxThunk就会去执行这个方法,以达到*控制action触发流程的一个目的。

ReudxThunk在实际项目中的应用

上面我们给计数器写的异步action只是为了作演示,简单地使用setTimeout来触发action。下面给出一个比较贴近现实的例子。

在某个项目中,我们需要根据一个userId来调用后端接口获取这个用户的详细信息并存储到Redux store中:

function getUserDetail (userId) {
return (dispatch, getState) => {
if (getState().user.id === userId) {
// store中的user已经为当前的目标user,无需重复获取
return;
}

dispatch({type: 'USER_DETAIL_REQUEST', payload: userId});
fetch(`${API_ROOT}/user/${userId}`)
.then(res => res.json())
.then(res => {
// 触发SUCCESS的action后在reducer中更新user数据
dispatch({type: 'USER_DETAIL_REQUEST_SUCCESS', payload: res});
})
.catch(err => dispatch({type: 'USER_DETAIL_REQUST_FAILURE', payload: err})
};
}

// 获取userId为10000的用户详情
store.dispatch(getUserDetail(10000));