Redux中间件

2016-03-25 Alex Sun 更多博文 » 博客 » GitHub »

原文链接 https://syaning.github.io/2016/03/25/redux-middleware/
注:以下为加速网络访问所做的原文缓存,经过重新格式化,可能存在格式方面的问题,或偶有遗漏信息,请以原文为准。


参考资料


Redux也有Middleware的概念,类似于Express或者Koa。事实上,Redux的中间件模型更接近Koa的,都是洋葱模型,而Express的中间件模型则是瀑布流模型。

关于Express和Koa的中间件机制,可以参考我之前的文章:

官方文档对中间件已经有了比较详细的解释。或者参考这个非常简单的例子来初步了解Redux中间件的使用。

下面通过对Redux的部分源码进行分析,来详细了解Redux的中间件机制。

1. compose

Redux中间件机制的核心方法是applyMiddleware,但是在了解该方法之前,先来分析依赖函数compose。源码如下:

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

    const last = funcs[funcs.length - 1]
    const rest = funcs.slice(0, -1)

    return rest.reduceRight((composed, f) => f(composed), last(...args))
  }
}

该方法接收一系列的函数作为参数,返回值是一个新的函数。其效果相当于函数的嵌套调用。例如:

compose(f, g, h)(...args)

就相当于:

f(g(h(...args)))

2. applyMiddleware

源码及分析如下:

export default function applyMiddleware(...middlewares) {
  /**
   * 返回值是一个函数,因此在使用的时候是如下形式:
   * var store = applyMiddleware(...midlewares)(createStore)(reducer)
   */
  return (createStore) => (reducer, initialState, enhancer) => {
    // 普通方式创建的store
    var store = createStore(reducer, initialState, enhancer)
    var dispatch = store.dispatch
    var chain = []

    /**
     * 将store的一部分接口暴露给中间件
     * 在编写中间件的时候,通常形式为:
     * function middleware(store){
     *     return next => action => { ... }
     * }
     * 参数的store其实就是middlewareAPI
     */
    var middlewareAPI = {
      getState: store.getState,
      /**
       * 这里之所以使用一个匿名函数,而不是`dispatch: dispatch`的形式
       * 是因为在中间件中执行store.dispatch的时候,dispatch的值已经改变
       * 在下面的代码中将会看到dispatch被重新赋值
       */
      dispatch: (action) => dispatch(action)
    }

    /**
     * chain是一个数组,数组中的每一项都是一个具有如下形式的函数:
     * function(next){
     *     return function(action){ ... }
     * }
     */
    chain = middlewares.map(middleware => middleware(middlewareAPI))

    /**
     * 假设chain为[mw1, mw2, mw3]
     * 那么此时dispatch为mw1(mw2(mw3(store.dispatch)))
     * 即store.dispatch作为mw3的next参数
     * mw3(store.dispatch)作为mw2的next参数,以此类推
     * 最终的返回值是一个函数,其形式为:
     * function(action){ ... }
     * 该函数作为新的dispatch(或者说,包装后的dispatch)
     */
    dispatch = compose(...chain)(store.dispatch)

    return {
      ...store,
      dispatch
    }
  }
}

3. 实例

假如有两个中间件,loggergreeting

function logger(store) {
    return function(next) {
        return function(action) {
            console.log('logger: dispatching:', action)
            var result = next(action)
            console.log('logger: get state:', store.getState())
            return result
        }
    }
}

function greeting(store) {
    return function(next) {
        return function(action) {
            console.log('greeting: Hi~ o(* ̄▽ ̄*)ブ')
            var result = next(action)
            console.log('greeting: ヾ( ̄▽ ̄)Bye~Bye~')
            return result
        }
    }
}

那么经过applyMiddleware后,新的dispatch函数其实是:

function dispatch(action) {
    console.log('logger: dispatching:', action)

    var result = (function(action) {
        console.log('greeting: Hi~ o(* ̄▽ ̄*)ブ')
        var result = store.dispatch(action)
        console.log('greeting: ヾ( ̄▽ ̄)Bye~Bye~')
        return result
    })(action)

    console.log('logger: get state:', store.getState())
    return result
}

如果有更多的中间件的话,这个过程会一层一层嵌套下去。

通过对这个例子的分析,可以非常直观地看出,中间件的作用,无非是捕获store.dispatch(action)这个动作,在它的之前和之后做一些其它事情。

4. 异步Action

通过前面的源码分析,我们可以知道,每个中间件所接收到的store参数,其实是真正Store的一个子集,只有dispatchgetState方法。那么,如果在某个中间件中调用了dispatch(action),岂不是就陷入无限循环了?事实上,中间件拿到dispatch,主要是用于异步操作。

下面看一个例子(完整源码)。

// action.js
function increaseCounterAsync() {
    return function(dispatch, getState) {
        setTimeout(() => dispatch({
            type: 'INCREASE'
        }), 3000)
    }
}

// middleware: thunk
function thunk(store) {
    return function(next) {
        return function(action) {
            return typeof action === 'function' ?
                action(store.dispatch, store.getState) :
                next(action)
        }
    }
}

// store
var store = applyMiddleware(thunk, logger)(createStore)(reducer)
store.dispatch(increaseCounterAsync())

注: 这里的thunk中间件其实就是redux-thunk的实现。

在这里,increaseCounterAsync()创建的action是一个函数,而thunk中间件会对action进行拦截,如果action是一个函数,则将dispatchgetState作为参数执行该函数,否则将该action按照普通流程来处理。也就是说,thunk中间件捕获到了异步action之后,进行一些处理,然后dispatch真正的action。

在上面的例子中,使用了thunk和logger两个中间件,此时dispatch函数其实是:

function dispatch(action) {
    if (typeof action === 'function') {
        // 这里的dispatch和getState其实是middlewareAPI的
        return action(dispatch, getState)
    }

    var result = (function(action) {
        console.log('logger: dispatching:', action)
        // 这里的store指的是applyMiddleware的内部变量store
        var result = store.dispatch(action)
        console.log('logger: get state:', store.getState())
        return result
    })(action)

    return result
}

可以看到,如果一个异步action的话,被thunk中间件捕获后,就不会执行到后面的logger部分。然后action(dispatch, getState)会重新dispatch一个同步action。