前面所接触到的 state 状态更新,都是同步更新,但实际上 state 状态更新并不是在用户发起操作的一刻就马上更新,往往还伴随着【请求服务器 -> 等待服务器响应 -> 处理服务器响应 -> 最后更新 state 】这样的一个流程。
一开始我有这样一个疑问,在异步操作完成之后,手动调用 dispatch 分发一个 action 似乎也可以完成对 state 的状态更新管理,但如果 dispatch() 过程中,还需要加一些额外的处理,而整个应用中有几十个异步操作,那不得对每个操作结果都手动进行处理,势必是不行的,不易维护且容易出错,但使用 Redux 的中间件 (middleware) 可以巧妙的解决这两个问题:数据处理和操作完成后自动更新 state。
使用中间件
redux-logger
redux-logger 用于打印日志,可以在开发过程中通过 log 清楚的跟踪 state 的改变。
1 2 3 4 5 6 7 8
| import { applyMiddleware, createStore } from 'redux'; import createLogger from 'redux-logger'; const logger = createLogger(); const store = createStore( reducer, applyMiddleware(logger) );
|
redux-logger 提供一个生成器 createLogger,可以生成日志中间件 logger。然后,将它放在 applyMiddleware 方法之中,传入 createStore 方法,就完成了store.dispatch() 的功能增强,在使用 store.dispatch() 分发一个 action 的时候,会自动打印出 action,以及 state 在改变前后的状态;
createStore 方法可以接受整个应用的初始状态作为参数,此时 applyMiddleware 就是第三个参数。
中间件是有次序的,比如 logger 中间件得放到最后。
1 2 3 4
| const store = createStore( reducer, applyMiddleware(thunk, promise, logger) );
|
applyMiddleware
applyMiddleware 是 Redux 的原生方法,作用是将所有的中间件组成一个数组,依次处理执行 action。下面是它的源码。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| export default function applyMiddleware(...middlewares) { return (createStore) => (reducer, preloadedState, enhancer) => { var store = createStore(reducer, preloadedState, enhancer); var dispatch = store.dispatch; var chain = []; var middlewareAPI = { getState: store.getState, dispatch: (action) => dispatch(action) }; chain = middlewares.map(middleware => middleware(middlewareAPI)); dispatch = compose(...chain)(store.dispatch); return {...store, dispatch} } }
|
所有中间件被放进了一个数组 chain,然后嵌套执行,最后执行 store.dispatch。可以看到,中间件内部(middlewareAPI)可以拿到 getState 和 dispatch 这两个方法。
异步操作流程
异步操作需要三种 action:
- 操作发起时的 Action
- 操作成功时的 Action
- 操作失败时的 Action
比如一个 http 请求
1 2 3 4 5 6 7 8 9
| { type: 'FETCH_POSTS' } { type: 'FETCH_POSTS', status: 'error', error: 'Oops' } { type: 'FETCH_POSTS', status: 'success', response: { ... } } { type: 'FETCH_POSTS_REQUEST' } { type: 'FETCH_POSTS_FAILURE', error: 'Oops' } { type: 'FETCH_POSTS_SUCCESS', response: { ... } }
|
- 操作开始时,送出一个 Action,触发 state 更新为”正在操作”状态,View 重新渲染
- 操作结束后,再送出一个 Action,触发 state 更新为”操作结束”状态,View 再一次重新渲染
异步操作之 redux-thunk
异步操作至少要两个 action,第一个是开始操作时,这是一个同步的 action,当操作完成后,是另外一个 action,第二个 action 我们不想手动的在操作完成的函数里面使用 store.dispatch() 去发,而是由中间件自动替我们发送。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
| const getPromiseObj = function () { return new Promise(function (resolve, reject) { var r = Math.random(); var timeout = 0; if (r < 0.5) { timeout = 500; } else { timeout = 1000; } setTimeout(function () { resolve(timeout); }, timeout); }); }; const begin = ()=>({ type: "BEGIN" }); const end = coast => ({ type: "END", coast }); const httpFetch = function () { return function (dispatch, getState) { dispatch(begin()); return getPromiseObj().then(num => { dispatch(end(num)); }); }; };
|
上面是一个异步操作,使用 timeout 来模仿网络请求。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36
| import React from "react"; import {connect} from 'react-redux'; import {httpFetch} from './actions.js'; const App = React.createClass({ componentDidMount: function () { this.props.httpFetch(); }, render() { return <div> <h2>Begin:</h2> <div>{this.props.begin}</div> <h2>Coast:</h2> <div>{this.props.coast}</div> <br /> </div> } }); const mapStateToProps = (state, ownProps) => { return { begin: state.begin, coast: state.coast } }; const mapDispatchToProps = (dispatch, ownProps) => ({ httpFetch: ()=> { dispatch(httpFetch()); } }); const ContainerApp = connect(mapStateToProps, mapDispatchToProps)(App); module.exports = ContainerApp;
|
在 App 被加载之后,使用 dispatch(httpFetch()) 发出一个 action,httpFetch() 的返回值就是一个 action。
- httpFetch() 是一个 Action Creator(动作生成器),它返回了一个函数,而普通的 Action Creator 默认返回一个对象。
- 返回的函数的参数是 dispatch() 和 getState() 这两个 Redux 方法,普通的 Action Creator 的参数是 Action 的内容。
- 在返回的函数被执行后,首先发出一个 Action,表示操作开始。
- 异步操作结束之后,再发出一个 Action,表示操作结束。
上面的处理方法,就解决了自动发送第二个 Action 的问题。但是,Action 是由 store.dispatch() 方法发送的。而 store.dispatch 方法正常情况下,参数只能是对象,不能是函数。此时,就需要使用中间件 redux-thunk,它会改造 store.dispatch,使它能够接受函数作为参数。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| const timer = function (state = {begin: 0, coast: 0}, action) { switch (action.type) { case "BEGIN": return Object.assign({}, state, { begin: new Date().getTime() }); case "END": return Object.assign({}, state, { coast: action.coast }); default: return state; } }; module.exports = timer;
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| import {createStore} from "redux"; import React from "react"; import ReactDOM from "react-dom"; import {Provider} from 'react-redux'; import {applyMiddleware} from 'redux'; import thunkMiddleware from "redux-thunk"; import createLogger from 'redux-logger'; import reducer from './reducer.js'; import ContainerApp from './ContainerApp.js'; const logger = createLogger(); const store = createStore(reducer, applyMiddleware(thunkMiddleware, logger)); ReactDOM.render(<Provider store={store}> <ContainerApp /> </Provider>, document.getElementById('root'));
|
异步操作的第一种解决方案就是,写出一个返回函数的 Action Creator,然后使用 redux-thunk 中间件改造 store.dispatch。