今天和大家聊一聊redux的中间件原理。

注:本文内容大部分参考redux的官网文档Middleware - Redux。如果英文好的同学可以直接阅读官网文档,写的非常好。


关于redux-middleware

redux中间件提供了一个切面的关注点。

我们可以很方便的利用中间件进行AOP编程,比如日志功能,埋点上报等。

这里主要是利用装饰器模式,在实际任务执行之前,动态添加before和after的逻辑。

这样就能形成一个洋葱模型

image-20200617153838528

接下来我们就看下如何手动开写一个日志中间件

手动处理日志

首先我们手动处理日志,就是在执行dispatch方法前后,添加console.log

console.log('dispatching', action)
store.dispatch(action)
console.log('next state', store.getState())

抽取方法

手动处理日志有一点问题,假如我们有多处执行action的地方需要用到日志,我们不可能每一处都进行复制粘贴。

那么这时候就需要额外抽取一个方法。

function dispatchAndLog(store, action) {
  console.log('dispatching', action)
  store.dispatch(action)
  console.log('next state', store.getState())
}

这样我们只要对原来执行dispatch的地方替换为这个方法,就可以实现日志功能了

dispatchAndLog(store, addTodo('Use Redux'))

Monkeypatching

上一层的方法虽然解决了重复代码的问题,但是还是需要我们修改所有的dispatch处。

这就会导致入侵业务代码。

那有没有非入侵的方式呢?有,就是Monkeypatching。

Monkeypatching,简单的来说就是用自己定义的新方法,替换对象的原始方法。

这样虽然业务代码(使用方)没有进行改动,但是实际的执行代码已经在运行时被更改了。

const next = store.dispatch
store.dispatch = function dispatchAndLog(action) {
  console.log('dispatching', action)
  let result = next(action)
  console.log('next state', store.getState())
  return result
}

处理多中间件问题

上一步我们解决了一个中间件的问题,假如我们现在需要添加一个新的中间件,那么应该怎么处理呢?

最简单的方法是复制一遍逻辑。

let next = store.dispatch
store.dispatch = function dispatchAndLog1(action) {
  console.log('dispatching1', action)
  let result = next(action)
  console.log('next state1', store.getState())
  return result
}

next = store.dispatch
store.dispatch = function dispatchAndLog2(action) {
  console.log('dispatching2', action)
  let result = next(action)
  console.log('next state2', store.getState())
  return result
}

显然,获取next和赋值store.dispatch都是重复逻辑,应该抽取出来成为公共代码.

假如我们做如下改动,直接返回包装后的函数会如何?

function logger(store) {
  const next = store.dispatch

  return function dispatchAndLog(action) {
    console.log('dispatching', action)
    let result = next(action)
    console.log('next state', store.getState())
    return result
  }
}

这样我们就可以使用一个foreach对一个中间件数组进行添加。

function applyMiddlewareByMonkeypatching(store, middlewares) {
  middlewares = middlewares.slice()
  middlewares.reverse()

  // Transform dispatch function with each middleware.
  middlewares.forEach(middleware => (store.dispatch = middleware(store)))
}

OK,这个解决方案已经很棒了,但是我们还可以更进一步。

试想一下,我们在中间件代码中,其实并不关心next方法是不是store.dispatch,只需要知道它能够链式处理action即可。

那么我们可以进一步隐藏这个概念。

const logger = store => next => action => {
  console.log('dispatching', action)
  let result = next(action)
  console.log('next state', store.getState())
  return result
}
function applyMiddleware(store, middlewares) {
  middlewares = middlewares.slice()
  middlewares.reverse()
  let dispatch = store.dispatch
  middlewares.forEach(middleware => (dispatch = middleware(store)(dispatch)))
  return Object.assign({}, store, { dispatch })
}

这样一来,我们在执行时,只要对一个dispatch中间变量进行处理,最后再赋值在store即可。

小结

在这里我们看到了如何一步步实现一个redux的中间件机制。

实际上,类似express,koa等后端框架的中间件机制也是用类似的方法进行处理的。

有了中间件,我们可以更方便的在非入侵业务代码的情况下实现更多复杂的功能。


参考文档:


本文会经常更新,请阅读原文: https://xinyuehtx.github.io/post/%E4%BB%8E0%E5%BC%80%E5%A7%8B%E5%AE%9E%E7%8E%B0redux%E4%B8%AD%E9%97%B4%E4%BB%B6%E6%9C%BA%E5%88%B6.html ,以避免陈旧错误知识的误导,同时有更好的阅读体验。

知识共享许可协议 本作品采用 知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议 进行许可。欢迎转载、使用、重新发布,但务必保留文章署名黄腾霄(包含链接: https://xinyuehtx.github.io ),不得用于商业目的,基于本文修改后的作品务必以相同的许可发布。如有任何疑问,请 与我联系