理解 Redux 的原理有助于我们更好的使用它。本文实现 redux 的多个中间件合并功能。
在上一篇文章中实现了 redux 的中间件机制,支持了传入一个中间件的用法,在实际的 redux 中,applyMiddleware 是支持传入多个中间件的,本文使用 redux 实现多个中间件合并。
本文完整代码请查看 Github:https://github.com/YanYuanFE/redux-app
// clone repo
git clone https://github.com/YanYuanFE/redux-app.git
cd redux-app
// checkout branch
git checkout part-6
// install
npm install
// start
npm start
中间件合并#
使用多个中间件的示例代码如下:
const store = createStore(
reducer,
applyMiddleware(thunk, promise, logger)
);
还是在原来的项目中进行开发,在 src 下 redux.js 中,对 applyMiddleware 函数进行修改。
export function applyMiddleware(...middlewares) {
}
单个中间件 middleware 的结构如下:
store => next => action => {
let result = next(action);
return result;
};
当传入多个 middlewares 参数时,将参数展开,middlewares 成为一个数组方便后面操作。
export function applyMiddleware(...middlewares) {
return createStore => (...args) => {
const store = createStore(...args);
let dispatch = store.dispatch;
const midApi = {
getState: store.getState(),
dispatch: (...args) => dispatch(...args)
}
const middlewareChain = middlewares.map(middleware => middleware(midApi));
dispatch = compose(...middlewareChain)(store.dispatch);
return {
...store,
dispatch
}
}
}
当有多个中间件时,对中间件数组 middlewares 执行 map 方法,对每个中间件都执行一次并传入 midApi,返回成为一个新的数组 middlewareChain。
middlewareChain 中保存着 middleware 执行一次后返回的函数 [mid1, mid2, mid3], 每个 mid 的结构如下:
next => action => {
let result = next(action);
return result;
};
然后,需要一个 compose 方法来对每个 mid 方法进行依次执行,并返回一个函数,最后传入 store.dispatch 参数。
compose 方法作用如下:
compose(fn1, fn2, fn3)
fn1(fn2(fn3)))
compose 传入一系列函数作为参数,然后将一系列函数参数嵌套依次进行调用。
下面是 compose 的实现:
function compose(...funcs) {
if (funcs.length === 0) {
return arg => arg;
}
if (funcs.length === 1) {
return funcs[0]
}
return funcs.reduce((ret, item) => (...args) => ret(item(...args)))
}
在 compose 方法中,传入一系列函数参数,展开,funcs 为一个数组。
当 funcs.length 为 0 时,即一个参数都没有的时候,返回一个默认函数;当传入一个参数时,直接返回第一个参数;当传入多个函数参数时,使用数组的 reduce 方法依次对 funcs 数组从左到右执行一个函数,该函数中,ret 为上一次执行该函数的返回值,如果没有指定初始值,第一次执行时为数组第一个参数,item 为当前正在处理的数组元素。
执行 compose (fn1, fn2, fn3),在 reduce 方法中的 ret 和 item 每次执行的结果如下:
第一次执行,ret 为 fn1,item 为 fn2, 返回 fn1(fn2());
第二次执行,ret 为 fn1(fn2()),item 为 fn3, 返回结果为 fn1 (fn2 (fn3())))。
到此,redux 现在已经支持多个中间件的用法了。
编写中间件进行测试#
为了测试多个中间件,这里我们再编写一个简单的中间件用于支持数组 action,在 src 目录下新建 redux.array.js。
代码如下:
const arrayThunk = ({dispatch, getState}) => next => action => {
if (Array.isArray(action)) {
action.forEach(v => dispatch(v))
}
return next(action)
}
export default arrayThunk;
上述代码中,定义了 arrayThunk 中间件,使用 Array.isArray 方法判断 action 是否为数组,如果是数组就遍历 action 依次 dispatch。
下面在原来的计数器应用中,增加一个按钮,使用 arrayThunk 中间件点击每次加 2。
修改 components/Counter.js 如下:
import React, { Component } from 'react';
import PropTypes from 'prop-types';
class Counter extends Component {
constructor(props) {
super(props);
this.incrementAsync = this.incrementAsync.bind(this);
this.incrementIfOdd = this.incrementIfOdd.bind(this);
}
incrementIfOdd() {
if (this.props.value % 2 !== 0) {
this.props.onIncrement();
}
}
incrementAsync() {
setTimeout(this.props.onIncrement, 1000);
}
render() {
const { value, onIncrement, onDecrement, incrementAsync, addTwice } = this.props;
console.log(this.props);
return (
<p>
Clicked: {value} times
{' '}
<button onClick={onIncrement}>
+
</button>
{' '}
<button onClick={onDecrement}>
-
</button>
{' '}
<button onClick={incrementAsync}>
Increment async
</button>
{' '}
<button onClick={addTwice}>
+2
</button>
</p>
)
}
}
Counter.propTypes = {
value: PropTypes.number.isRequired,
onIncrement: PropTypes.func.isRequired,
onDecrement: PropTypes.func.isRequired,
incrementAsync: PropTypes.func.isRequired,
addTwice: PropTypes.func.isRequired,
};
export default Counter;
在 Counter.js 中,增加一个按钮,点击触发 props 中的 addTwice 方法。
在 src 下,App.js 中,修改代码如下:
import React, { Component } from 'react';
import { connect } from './react-redux';
import Counter from './components/Counter';
import logo from './logo.svg';
import './App.css';
class App extends Component {
render() {
const { onIncrement, onDecrement, counter, incrementAsync, addTwice } = this.props;
return (
<div className="App">
<header className="App-header">
<img src={logo} className="App-logo" alt="logo" />
<h1 className="App-title">Welcome to React</h1>
</header>
<p className="App-intro">
To get started, edit <code>src/App.js</code> and save to reload.
</p>
<Counter
value={counter}
onIncrement={onIncrement}
onDecrement={onDecrement}
incrementAsync={incrementAsync}
addTwice={addTwice}
/>
</div>
);
}
}
const mapStateToProps = (state) => ({
counter: state
});
function onIncrement() {
return { type: 'INCREMENT' }
}
function addTwice() {
return [{ type: 'INCREMENT' }, { type: 'INCREMENT' }]
}
function onDecrement() {
return { type: 'DECREMENT' }
}
function incrementAsync() {
return (dispatch, getState) => {
setTimeout(() => {
dispatch(onIncrement());
}, 2000)
}
}
const mapDispatchToProps = {
onIncrement,
onDecrement,
incrementAsync,
addTwice,
};
export default connect(mapStateToProps, mapDispatchToProps)(App);
增加了 addTwice 方法,发起一个数组 action,在数组中,定义了两个增加计数的 action,并加入 mapDispatch 中,在 App 组件中,从 props 中获取到 addwice 方法并传入 Counter 组件。
在 index.js 中,需要引入 arrayThunk 中间件。
import React from 'react';
import ReactDOM from 'react-dom';
import thunk from './thunk';
import arrThunk from './redux-array';
import { Provider } from './react-redux';
import './index.css';
import App from './App';
import {applyMiddleware, createStore} from './redux';
import counter from './reducers';
const store = createStore(counter, applyMiddleware(thunk, arrThunk));
ReactDOM.render(
<Provider store={store}>
<App/>
</Provider>,
document.getElementById('root')
);
在 index.js 中,引入 arrayThunk 并传入 applyMiddleware 中。
npm start 启动项目,打开浏览器进行操作,结果如下,达到预期效果。