再探Redux Middleware

前言

在初步了解Redux中间件演变过程之后,继续研究Redux如何将中间件结合。上次将中间件与redux硬结合在一起确实有些难看,现在就一起看看Redux如何加持中间件。

  • 中间件执行过程

希望借助图形能帮助各位更好的理解中间件的执行情况。

  • redux如何加持中间件

现在是时候看看redux是如何将中间件结合了,我们在源码中一探究竟。

* @param {Function} [enhancer] The store enhancer. You may optionally specify it * to enhance the store with third-party capabilities such as middleware,
 * time travel, persistence, etc. The only store enhancer that ships with Redux
 * is `applyMiddleware()`.
 *
 * @returns {Store} A Redux store that lets you read the state, dispatch actions
 * and subscribe to changes.
 */
export default function createStore(reducer, preloadedState, enhancer) {
  if (
    (typeof preloadedState === 'function' && typeof enhancer === 'function') ||
    (typeof enhancer === 'function' && typeof arguments[3] === 'function')
  ) {
    throw new Error(
      'It looks like you are passing several store enhancers to ' +
        'createStore(). This is not supported. Instead, compose them ' +
        'together to a single function'
    )
  }

  if (typeof preloadedState === 'function' && typeof enhancer === 'undefined') {
    enhancer = preloadedState // 如果初始化state是一个函数,则认为有中间件
    preloadedState = undefined
  }

  if (typeof enhancer !== 'undefined') {
    if (typeof enhancer !== 'function') {
      throw new Error('Expected the enhancer to be a function.')
    }

    return enhancer(createStore)(reducer, preloadedState)
  }

如果createStore第二个参数是函数(第二,第三都是函数会抛异常),则redux认为第二个参数是调用applyMiddleware函数的返回值(注释有说明)。

根据return enhancer(createStore)reducer, preloadedState),说明applyMiddleware返回了一个函数,该函数内还返回了一个函数。那么接下来从applyMiddleware源码中一探究竟。

export default function applyMiddleware(...middlewares) { // 将所有中间件存入middlewares数组 return createStore => (...args) => { // 返回函数以createStore为参数,args即[reducer, preloadedState]
    const store = createStore(...args) // 创建一个store
    let dispatch = () => { // 定义一个dispatch变量指向匿名函数,如果被调用则抛出异常 throw new Error(
        `Dispatching while constructing your middleware is not allowed. ` +
          `Other middleware would not be applied to this dispatch.`
      )
    }

    const middlewareAPI = {
      getState: store.getState,
      dispatch: (...args) => dispatch(...args) // middlewareAPI的dispatch属性指向一个匿名函数,该函数内部会执行外部dispatch变量指向的那个函数。
    }
    const chain = middlewares.map(middleware => middleware(middlewareAPI)) // 执行每个中间件,顺带检查是否有中间件调用传入参数中的dispatch,如果有则抛出异常
    dispatch = compose(...chain)(store.dispatch) // 将chain展开传入compose,然后执行返回的函数,传入store.dispatch,最后将所有中间件组合成最终的中间件,并将dispatch变量指向这个中间件。   // 由于dispatch变量的更改,它原来指向的匿名函数现在没有任何变量指向它,会被垃圾回收。
// 误区:调用middlewareAPI的dispatch属性指向的函数时,内部的dispatch会指向原来抛出异常的匿名函数。这是错误的,在调用middlewareAPI的dispatch属性所指向的函数时,
  // 会寻找dispatch变量,函数内部找不到就向外部作用域寻找,然后找到外部dispatch,而此时外部的dispatch指向最终的中间件,所以会调用最终的中间件。这对于理解redux-thunk非常重要。
return { ...store, dispatch // 覆盖store中dispatch变量 } } }

上面的代码中还有一点疑惑,compose函数是什么样子,那么我们再探compose。

 * @param {...Function} funcs The functions to compose.
 * @returns {Function} A function obtained by composing the argument functions
 * from right to left. For example, compose(f, g, h) is identical to doing * (...args) => f(g(h(...args))). 可以发现,和我们之前写的代码效果一模一样
 */

export default function compose(...funcs) {
  if (funcs.length === 0) {
    return arg => arg
  }

  if (funcs.length === 1) {
    return funcs[0]
  }

  return funcs.reduce((a, b) => (...args) => a(b(...args)))
}

也许你对数组的reduce方法不是很熟,上篇文章篇幅也比较饱满。那么这儿简单讲解下:

[1, 2, 3, 4].reduce((a, b) => { console.log(a, b); return a + b })
// 1 2 可以发现第一次执行,我们拿到数组的第1,2个变量
// 3 3 拿到上次返回的结果和第3个变量
// 6 4 拿到上次返回的结果和第4个变量

最后结果为10,没有打印所以看不出。当然数组存储的也可能是对象,在reduce函数执行时,拿到每个变量的副本(浅拷贝),然后根据你的代码做对应的事。在这就以上篇文章的中间

件为例,再加入logMiddleware3(和logMiddleware2类似,只是将打印的数字部分改为3而已),看看compose函数执行过程。

[logMiddleware3, logMiddleware2, logMiddleware].reduce((a, b) => (...args) => a(b(...args)))
// 假定compose函数传入的参数为store.dispatch,则有以下结果:
// (logMiddleware3, logMiddleware2) => (...args) => logMiddleware3(logMiddleware2(...args)) 这里args[0]为logMiddleware(store.dispatch)返回的中间件
// (logMiddleware3(logMiddleware2(...args)), logMiddleware) => (...args) => logMiddleware3(logMiddleware2(logMiddleware(...args))) 这里的args[0]为store.dispatch
// 最后返回(...args)=> logMiddleware3(logMiddleware2(logMiddleware(...args))) ,接着执行该函数,传入store.dispatch,也就产生了最终的中间件

现在对于redux结合过程已经有了一定的认识,是时候看看别人的中间件了,对比我们自己的中间件,也许有不同的收获。

  • redux-thunk

至此我们写的中间件都比较好理解,是时候认识下redux-thunk了。它又会有什么特别之处了,让我们一起看看源码。

function createThunkMiddleware(extraArgument) { // 这里extraArgument完全没用到 return ({ dispatch, getState }) => next => action => { // 这里的dispatch如果有疑惑,请看上面👆applyMiddleware源码解析 if (typeof action === 'function') {
      return action(dispatch, getState, extraArgument);
    }

    return next(action);
  };
}

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

export default thunk;

what?太精辟了有木有。其实理解起来也很简单,如果传入的action是一个函数,则调用该函数;否则调用上一个中间件并返回结果。当然你还可以再精简些。

return typeof action === 'function' ? action(dispatch, getState, extraArgument) : next(action)

那么问题来了,什么时候用得着redux-thunk呢?也就是什么情况下action会是函数。我们仔细看看action为函数时,它的参数也就明白了。在执行action函数时,我们还能调用dispatch,说明dispatch操作是要等待某个东西执行完才可以执行。说到这,还能是什么呢?当然非异步任务莫属了。

好了现在我们将原来的代码更改下,实现和redux,redux-thunk结合,这里我们先自己实现redux-thunk。

function ThunkMiddleware() {
  return ({ dispatch, getState }) => next => action => {
    return typeof action === 'function' ? action(dispatch, getState) : next(action)
  }
}

const thunk = ThunkMiddleware();
export default thunk;

新建middleware目录,新建redux-thunk和redux-logger,接着封装redux-logger模块。

function LoggerMiddleware() {
  return ({ getState }) => next => action => {
    console.log('dispatch: ', action);
    let result = next(action);
    console.log('nextState: ', getState());
    return result;
  }
}

const logger = LoggerMiddleware();
export default logger;

更改index.js。

import React from 'react';
import ReactDOM from 'react-dom';
import { createStore, applyMiddleware } from 'redux';
import { Provider } from 'react-redux';
import App from './App';
import * as serviceWorker from './serviceWorker';
import reduxLogger from './middlewares/redux-logger';
import reduxThunk from './middlewares/redux-thunk';

function listReducer(state = { list: [] }, action) {
  switch (action.type) {
    case 'receive':
      return {
        list: action.data
      };
    default:
      return state;
  }
}

const store = createStore(listReducer, applyMiddleware(reduxLogger, reduxThunk));

ReactDOM.render(
  <Provider store={store}>
    <App />
  </Provider>, document.getElementById('root'));

serviceWorker.unregister();

更改App.js。

import React, { Component } from 'react';
import { connect } from 'react-redux';
import axios from 'axios';
import Mock from 'mockjs';

Mock.mock('http://test.com/search', {
  'list|0-5': [{
    'id|+1': 1,
    name: '@character("lower")',
    'version': '@float(1, 10, 2, 2)',
    publisher: '@cname'
  }]
});

class App extends Component {
  state = {
    searchValue: '',
  };
  handleSearch = e => {
    e.preventDefault();
    if (this.state.searchValue) {
      this.props.changeList(this.state.searchValue);
    }
  };
  changeValue = e => {
    this.setState({ searchValue: e.target.value });
  };
  render() {
    return (
      <div style={{ textAlign: 'center', margin: '40px' }}>
        <form onSubmit={this.handleSearch}>
          <input type="text" value={this.state.searchValue} onChange={this.changeValue} />
          <button type="submit">搜索</button>
        </form>
        <ul>
          {this.props.list.map(item => (
            <li key={item.id} style={{ listStyle: 'none' }}>
              <p>{item.name}</p>
              <p>
                {item.publisher} publish {item.version}
              </p>
            </li>
          ))}
        </ul>
      </div>
    );
  }
}

const fetchResult = (searchValue) => dispatch => {
  return axios.get(`http://test.com/search`).then(result => {
    if (result.status === 200) {
      const data = result.data.list.map(item => ({...item, name: `${searchValue}${item.name}`}));
      const action = { type: 'receive', data };
      dispatch(action);
    }
  })
};

function mapStateToProps(state) {
  return {
    list: state.list
  }
}

function mapDispatchToProps(dispatch) {
  return {
    changeList: searchValue => dispatch(fetchResult(searchValue))
  }
}

export default connect(mapStateToProps, mapDispatchToProps)(App);

项目目录如下:

  • 结语

如果你理解了redux处理中间件的过程,那本文的目的也达到了。海纳百川,有容乃大。redux正是中间件的加持,才变得越发强大。也希望我们每天能进步一点点,造就更美好的自己。