如何编写一个 Redux 中间件

3,785 阅读4分钟

要想实现一个 redux 中间件我们必须了解 redux 的基本实现原理。本文将从 redux 源码入手,重点讲解 applyMiddleware 如何将中间件串联执行。只有理解了底层原理我们才可以游刃有余的写出一个 redux 中间件。

目录

createStore 源码解读

redux 通过 createStore 来创建一个 store 对象

要理解 applyMiddleware 的实现原理,我们要从 createStore 入手

export default function createStore(reducer, preloadedState, enhancer) {
  if (typeof preloadedState === 'function' && typeof enhancer === 'undefined') {
    enhancer = preloadedState
    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)
  }

  if (typeof reducer !== 'function') {
    throw new Error('Expected the reducer to be a function.')
  }
  // 篇幅有限,后面被我省略了,有兴趣请去看 redux 源码
  // ......

可以看见 createStore 的三个参数依次为: reducer, preloadedState, enhancer。参见源码,如果传入了 enhance 参数且为函数,则将 createStore 传入 enhance

return enhancer(createStore)(reducer, preloadedState)

也就是说,现在我们将用 enhance 来创建一个 store 对象。

applyMiddlewave 源码解读

一般情况下 createStore 的第三个参数 enhance 就是 applyMiddlewave

applyMiddlewave 的代码只有二十多行却是本文的重点

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
    }
  }
}

参见 createStore 的源码可以得知:applyMiddlewave 依然使用 createStore 创建了store 对象并且返回,只是改写了这个对象的 dispatch 方法。

下面我们重点来看这个被改写掉的 dispatch 方法,同时理解它和原生 dispatch 方法的区别也是本文的重点。为了更直观的了解这个过程我们先来看一个 简单的中间件实现 logger middlewave

export default store => next => action => {
    const start = Date.now();
    next(action);
    const ms = Date.now() - start;
    console.log(`dispatch: ${action.type} - ${ms}ms`);
}

下面分二步详细探讨中间件的运行原理

  1. 将原生的 getState 和 dispacth 作为第一个参数传入中间件数组,获得执行完的 chain 数组;

    chain = middlewares.map(middleware => middleware(middlewareAPI))

  2. 组合串联 middlewave

    dispatch = compose(...chain)(store.dispatch)

    compose 将所有的中间件串联起来组成新的 dispatch

    compose 源码

    function compose(...funcs) {
        return arg => funcs.reduceRight((composed, f) => f(composed), arg);
    }
    

    参考我们的 logger middlewave 这里的 composed 即是我们的 next 参数。

    reduceRight 和 ruduce 一样,不过 reduceRight 是从数组的右端开始执行,arg 将作为 reduceRight 的初始值(这里就是 store.dispatch)。假设我们的 chain 数组为 [f1,f2,f3]执行完毕后 dispatch 为 dispatch = f1(f2(f3(store.dispatch)))),调用这个新的 dispatch 每个中间件就能依次执行了,这里的中间件执行过程也是类似于 Koa 的中间件是非常经典的洋葱模型。只有最后一个中间件会触发 redux 原生的 dispatch,将这个 action 分发出去。(没错,我就是灵魂画师)

洋葱模型

redux-thunk 的实现原理

一般而言 dispatch 只能分发一个 action 对象,但是使用了 redux-thunk 中间件我们却可以分发一个异步函数。

const thunk = store => next => action => {
    typeof action === 'function' ?
        action(store.dispatch,store.getState) :
        next(action)
}

一个异步的 action 的示例

function getMessage = (dispatch, getState) => {
    axios.get('xxx/xxx')
    .then((res) => {
        dispatch({
            type: 'GET_MESSAGE_SUCCESS',
            message: res.json(),
        });
    })
    .catch((err) => {
        dispatch({
            type: 'GET_MESSAGE_ERROR',
            message: 'error'
        });
    });
}

这里的 dispatch 任然是改造后的 dispatch 因为传入中间件的第一个参数 store 即 middlewareApi 中的 dispatch 是一个闭包保存着对最外层函数 dispatch 的引用,所以当 diapatch 被改写后后面调用的 dispatch 都是这个新的 dispatch(即中间件的串联),所以即使在异步 action 中分发一个 action 依然会将全部中间件再执行一遍。

如何编写一个中间件

所以理解了以上,编写一个中间件将超级简单,只需要按照中间件编写规范

function myMiddleware = store => next => action = {
    // 在这里你可以拿到 store.getState 和 store.dispatch
    // 注意如果你调用 store.dispatch 中间件又从新从最外层开始 如果不加限制条件将陷入死循环
    // do something
    next(action)   // 进入下一个中间件,最后一个中间件的 next 参数为 redux 原生 dispatch
    // 返回继续执行这个中间件剩余部分
}

总结

深入理解 redux 中间件的实现原理,可以让我们在日常工作中,对 redux 数据流向更加清晰和对自己的程序更加有把握。本人水平有限,如有错误还请指出。

参考资料

redux 官方文档

《深入 react 技术栈》

阮一峰 redux 入门教程

原创文章,转载请注明原地址

如果你喜欢的话,可不可以给我点个小心心(*^__^*) 嘻嘻……