天天看点

Redux源码分析--中间件篇Happy Coding and Writing!!!

上一篇文章介绍了Redux的数据中心,并分别讲解了数据中心为开发者提供的各种接口,了解到要触发状态的更新就需要调用

dispatch

方法来分发

action

。然而

store

提供的

dispatch

方法只能够用来分发特定格式的

action

Redux源码分析--中间件篇Happy Coding and Writing!!!
如果我们想要更强大的功能怎么办?如果我们想要打印状态变化前后的日志?或者说想自定义某类方法来作为

dispatch

的参数?我们当然可以重写原来的

dispatch

方法,然而这并不优雅,维护成本相对较高。这篇文章想详细讲解一下,在函数式编程的背景下如何以中间件的方式优雅地扩展我们的

dispatch

方法。

1. 中间件

中间件这个概念存在于许多流行的Web框架中,可以把它想象成是请求/响应分发的中间层,用于对请求/响应做进一步的处理,而无需改变原有的代码逻辑。在

node.js

社区的

KOA

轻量级框架很出色地体现了这一点(当然它肯定不是第一个这样干的人)。koa本身只提供了最基础的请求/响应功能,如果想要更强大的功能(比如说日志,时间记录等功能)则需要自己添加相应的中间件。

Redux继承了这一理念,它把中间件应用到了

dispatch

方法的扩展中,让我们可以优雅地扩展

dispatch

方法,而不需要重写原有的

dispatch

方法,接下来我们好好体会一下它的精妙之处。

2. 中间件在Redux中的应用

在分析源码之前先来看看在Redux里面如何使用中间件,最关键的是

applyMiddleware

这个方法

import { createStore, applyMiddleware } from 'redux'
// Add thunk
import thunk from 'redux-thunk'
import logger from 'redux-logger'

const reducer = (state) => state

let newCreateStore = applyMiddleware(
  logger,
  thunk
)(createStore)

// 创建store,数据中心
let store = newCreateStore(reducer)           

其中

thunk

logger

就是我们提到的中间件,依次把它们传入

applyMiddleware

函数中,就会返回一个新的函数,然后再用这个函数处理原始的

createStore

方法就会返回一个增强过的

createStore

另外,还记得

createStore

函数可以接收

enhancer

这个参数不?其实

applyMiddleware

这个方法经过调用后所得到的就是一个增强器。为此我们还可以这样调用

createStore

,并生成

store

....
let enhancer = applyMiddleware(
  logger,
  thunk
)

let store = createStore(reducer, enhancer)           

这种做法跟前面的扩展效果是一样的。

3. 源码分析

1) 中间件原理

在源码分析之前,先举个例子来看看一个简单的

中间件

内部应该是什么样子的,我分别定义

middleware1

middleware2

两个中间件(他们本质是高阶函数),并用来扩展

originDispatch

函数

let originDispatch = (...args) => {
  console.log(...args)
}

const middleware1 = (dispatch) => {
  return (...args) => {
    console.log('middleware1 before dispatch')
    dispatch(...args)
    console.log('middleware1 after dispatch')
  }
}

const middleware2 = (dispatch) => {
  return (...args) => {
    console.log('middleware2 before dispatch')
    dispatch(...args)
    console.log('middleware2 before dispatch')
  }
}

originDispatch = middleware2(middleware1(originDispatch))
originDispatch('ruby', 'cool', 'language')
           

结果如下

middleware2 before dispatch
middleware1 before dispatch
ruby cool language
middleware1 after dispatch
middleware2 before dispatch
           

是不是运行过程是不是有点像洋葱?我们可以使用中间件来对原有的方法进行增强,并返回一个增强了的方法,然后再用另一个中间件来对这个已经增强过的方法再进一步增强,模型示意图如下

Redux源码分析--中间件篇Happy Coding and Writing!!!

2) compose--方法封链辅助函数

从上面的洋葱模型可以看出我们如果要增强一个方法,它的步骤如下

newFunc = f1(f2(func))           

可以简单地把

f1

,

f2

理解成我们需要各自定义的中间件函数,然而如果我们每次都要手动调用这些方法的话似乎并不太优雅,这个时候可以使用

compose

函数来完成这种事情。

compose

在中文里面是组合的意思,Redux所定义的

compose

函数可以把函数的参数列表构造成依次调用的形式,并返回一个新的函数。它的源码如下

export default function compose(...funcs) {
  ...
  // 以上都是判断

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

文字解释可能还不如流程图来得直观,下面简单地分析一下

compose(f1, f2, f3, f4)

的调用过程

a: f1, b: f2, return: (...args) => f1(f2(...args))
a: (...args) => f1(f2(...args)), b: f3, return: (...args) => f1(f2(f3(...args)))
a: (...args) => f1(f2(f3(...args))), b: f4, return: (...args) => f1(f2(f3(f4(...args))))
           

把这个方法应用在最初的例子中

> newfunc = compose(middleware2, middleware1)(originDispatch)
[Function]
> newfunc('node', 'good', 'languate')

middleware2 before dispatch
middleware1 before dispatch
node good languate
middleware1 after dispatch
middleware2 before dispatch
           

结果是一样的。而且从这个例子还可以看出在

compose

函数的参数列表中越靠后的函数,在构造完成之后,距离原始函数就越近。

3) applyMiddleware--收集中间件,扩展createStore

applyMiddleware.js

这个文件里面就包含着它的源码

export default function applyMiddleware(...middlewares) {
  return createStore => (...args) => {
    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.`
      )
    }

    const middlewareAPI = {
      getState: store.getState,
      dispatch: (...args) => dispatch(...args)
    }

    // #1 中间件应该接收store
    const chain = middlewares.map(middleware => middleware(middlewareAPI))

    // #2 返回的函数用于处理dispatch函数
    dispatch = compose(...chain)(store.dispatch)

    // #3 替换dispatch
    return {
      ...store,
      dispatch
    }
  }
}
           

代码片段

#2

中我们传入

compose

函数里的所有函数都是用于扩展

dispatch

的,这些函数会被定义为这种形式

(dispatch) => {
  return function(...args) {
    // do something before
    dispatch(...args)
    // do something after
  }
}           

这些函数会接收一个

dispatch

方法为参数,并返回一个增强的

dispatch

方法。然而我们需要编写的中间件却不仅如此,接下来再看看代码片段

#1

,以及相关的上下文逻辑

export default function applyMiddleware(...middlewares) {
  ....
  const middlewareAPI = {
    getState: store.getState,
    dispatch: (...args) => dispatch(...args)
  }

  // #1 中间件应该接收store
  const chain = middlewares.map(middleware => middleware(middlewareAPI))

  // #2 返回的函数用于处理dispatch函数
  dispatch = compose(...chain)(store.dispatch)
  ...
}
           

我们通过

map

方法来处理

applyMiddleware

所接收的所有中间件,让他们分别以

middlewareAPI

这个对象作为参数调用过后会返回一个新的函数列表,而这个函数列表才是真正用来增强

dispatch

的。

middlewareAPI

是仅仅包含了

getState

dispatch

这两个字段的对象,可以把它看成是一个精简版的

store

。因此我们需要编写的中间件应该是以

store

作为参数,并且返回一个用于增强

dispatch

方法的函数,而这个

store

我们只能够使用

getState

dispatch

这两个接口。听起来有点拗口,下面我们自行编写一个用于打印状态日志的中间件。

const Logger = (store) => (dispatch) => {
  return function(...args) {
    const wrappedDispatch = store.dispatch
    const getState = store.getState

    console.log('before dispatch', getState())
    dispatch(...args)
    console.log('after dispatch', getState())

    console.info(dispatch)
    console.info(wrappedDispatch)
  }
}
           

dispatch

wrappedDispatch

所指代的分发方法是不一样的。

dispatch

是从参数中传入,如果当前中间件是第一个对

dispatch

方法进行增强的中间件,则当前的

dispatch

所指向的就是Redux原生定义的

dispatch

方法。如果当前中间件前面已经有若干中间件的调用,则当前

dispatch

所指代的是经过前面中间件加强过的新的

dispatch

方法。我们可以来验证一下

let enhancer = applyMiddleware(
  Logger, // 我们自己编写的Logger
  thunk
)           

dispatch

的打印结果如下

ƒ (action) {
  if (typeof action === 'function') {
    return action(dispatch, getState, extraArgument);
  }

  return next(action);
}
           

可见,这是一个经过

thunk

中间件处理后返回的方法。

wrappedDispatch

因为匿名函数

(...args) => dispatch(...args)

的关系,在

applyMiddleware

函数运行完成并返回之后,匿名函数内部的

dispatch

会始终指向经过我们增强的

dispatch

方法。也就是说在中间件里面执行

store.dispatch

就会始终运行最外层的被增强过的

dispatch

方法。模型如下

Redux源码分析--中间件篇Happy Coding and Writing!!!

wrappedDispatch

打印结果虽然看不出什么,但我也顺手贴一下吧

ƒ dispatch() {
  return _dispatch.apply(undefined, arguments);
}           

接下来,我们看

applyMiddleware

的返回值,它会返回一个新的函数,该函数会以

createStore

作为参数,处理过后返回一个新的

createStore

方法,它的模式大概是这样子

(createStore) => (...args) => {

  // createStore方法用来创建store
  return {
    ...
    getState: ...
    dispatch: ...
  }
}           

而在

applyMiddleware

中实际上我们只需要增强

dispatch

方法,为此我们只需要用新的

dispatch

方法来替换原来的便可。代码片段

#3

就是用新的

dispatch

方法取代原来

store

中的

dispatch

....
    return {
      ...store,
      dispatch
    }
....           

4. 尾声

本章着重介绍了Redux中的中间件的原理,我们可以通过洋葱模型来增强

dispatch

函数。还可以通过

compose

方法构造调用链,使得我们的调用逻辑更加优雅。分析的过程中我还顺手编写了一个

Logger

中间件,用于打印

action

分发前后的状态,我们也可以根据自己的需求来编写属于自己的中间件。

Happy Coding and Writing!!!

原文发布时间为:2018年06月22日

原文作者:lanzhiheng

本文来源: 

掘金 https://juejin.im/entry/5b3a29f95188256228041f46

如需转载请联系原作者