「React」问:redux中间件原理

246 阅读4分钟

Redux源码中只有同步action,也就是说当dispatch action之后,state就会被立即更新。

那么如果想在redux引用异步数据流,就应当引入中间件。

redux中间件将会在 action 被分发之后、到达 reducer 之前执行。

中间件的执行前提,即 applyMiddleware 将会对 dispatch 函数进行改写,使得 dispatch 在触发 reducer 之前,会首先执行对 Redux 中间件的链式调用

Redux 中间件机制是如何实现的

Redux 中间件是通过调用 applyMiddleware 来引入的

// applyMiddlerware 会使用“...”运算符将入参收敛为一个数组

export default function applyMiddleware(...middlewares) {
  // 它返回的是一个接收 createStore 为入参的函数
  return createStore => (...args) => {
    // 首先调用 createStore,创建一个 store
    const store = createStore(...args)
    let dispatch = () => {
      throw new Error(
        `Dispatching while constructing your middleware is not allowed. ` + `Other middleware would not be applied to this dispatch.`
      )
    }
    // middlewareAPI 是中间件的入参
    const middlewareAPI = {
      getState: store.getState,
      dispatch: (...args) => dispatch(...args)
    }
    // 遍历中间件数组,调用每个中间件,并且传入 middlewareAPI 作为入参,得到目标函数数组 chain
    const chain = middlewares.map(middleware => middleware(middlewareAPI))
    // 改写原有的 dispatch:将 chain 中的函数按照顺序“组合”起来,调用最终组合出来的函数,传入 dispatch 作为入参
    dispatch = compose(...chain)(store.dispatch)
    // 返回一个新的 store 对象,这个 store 对象的 dispatch 已经被改写过了
    return {
      ...store,
      dispatch
    }
  }
}

在这段源码中,我们着重需要搞清楚的是以下几个问题:

  • applyMiddleware 返回了一个什么样的函数?这个函数是如何与 createStore 配合工作的?
  • dispatch 函数是如何被改写的?
  • compose 函数是如何组合中间件的?

1、applyMiddleware 是如何与 createStore 配合工作的?

applyMiddleware返回的是一个接受createStore为入参的嵌套函数。这个函数将会作为入参传递给 createStore,enhancer是dispatch增强器。

function createStore(reducer, preloadedState, enhancer) {
    // 这里处理的是没有设定初始状态的情况,也就是第一个参数和第二个参数都传 function 的情况
    if (typeof preloadedState === 'function' && typeof enhancer === 'undefined') {
        // 此时第二个参数会被认为是 enhancer(中间件)
        enhancer = preloadedState;
        preloadedState = undefined;
    }
    // 当 enhancer 不为空时,便会将原来的 createStore 作为参数传入到 enhancer 中
    if (typeof enhancer !== 'undefined') {
        return enhancer(createStore)(reducer, preloadedState);
    }
}

从上面的代码可以看出,一旦发现 enhancer 存在(对应到中间件场景下,enhancer 指的是 applyMiddleware 返回的函数),那么 createStore 内部就会直接 return 一个针对 enhancer 的调用。在这个调用中,第一层入参是 createStore,第二层入参是 reducer 和 preloadedState。

2.dispatch 函数是如何被改写的?

// middlewareAPI 是中间件的入参
const middlewareAPI = {
  getState: store.getState,
  dispatch: (...args) => dispatch(...args)
}

// 遍历中间件数组,调用每个中间件,并且传入 middlewareAPI 作为入参,得到目标函数数组 chain
const chain = middlewares.map(middleware => middleware(middlewareAPI))
// 改写原有的 dispatch:将 chain 中的函数按照顺序“组合”起来,
// 调用最终组合出来的函数,传入 dispatch 作为入参
dispatch = compose(...chain)(store.dispatch)

这个代码片段做了两件事:首先以 middlewareAPI 作为入参,逐个调用传入的 middleware,获取一个由“内层函数”组成的数组 chain;然后调用 compose 函数,将 chain 中的“内层函数”逐个组合起来,并调用最终组合出来的函数。

按照约定,所有的 Redux 中间件都必须是高阶函数。在高阶函数中,我们习惯于将原函数称为“外层函数”,将 return 出来的函数称为“内层函数”。

而 apply 中遍历 middlewares 数组,逐个调用 middleware(middlewareAPI),无非是为了获取中间件的内层函数。

外层函数的主要作用是获取 dispatch、getState 这两个 API,而真正的中间件逻辑是在内层函数中包裹的。待middlewares.map(middleware => middleware(middlewareAPI))执行完毕后,内层函数会被悉数提取至 chain 数组

提取出 chain 数组之后,applyMiddleware 做的第一件事就是将数组中的中间件逻辑 compose 起来。

3. compose 源码解读:函数的合成

// compose 会首先利用“...”运算符将入参收敛为数组格式
export default function compose(...funcs) {
  // 处理数组为空的边界情况
  if (funcs.length === 0) {
    return arg => arg
  }

  // 若只有一个函数,也就谈不上组合,直接返回
  if (funcs.length === 1) {
    return funcs[0]
  }
  // 若有多个函数,那么调用 reduce 方法来实现函数的组合
  return funcs.reduce((a, b) => (...args) => a(b(...args)))
}

compose源码的核心是最后一行,调用reducer函数去依次把数组中的函数组合到一个函数中去。