redux的中间件applyMiddleware源码

原文: redux的applyMiddleware源码

记得之前第一次看redux源码的时候是很懵逼的,尤其是看到applyMiddleware函数的时候,更是懵逼。当然那也是半年前的事情了,前几天把redux源码看了下,并且实现了个简单的redux功能。但是没有实现中间件。今天突然又想看看redux的中间件,就看了起来。

记得半年之前是从函数声明的下一行就开始看不懂了。。。然后前段时间,看了下柯里化函数,加深了高阶函数的印象,所以今天打算把中间件的源码给撸一下。

我们来看看函数声明的下一行,也就是源码第二行开始看:

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)
    }
    const chain = middlewares.map(middleware => middleware(middlewareAPI))
    dispatch = compose(...chain)(store.dispatch)

    return {
      ...store,
      dispatch
    }
  }
}

从上面我们可以看到中间件返回了函数,返回第一个函数是携带createStore参数的,这个是啥?从名字上我们就可以知道,就是createStore。不过为了证明,我们还是得从源码上来看。

还记得是怎么调用的中间件的吧,大致如下:

const store = createStore(
    reducer,
    applyMiddleware(...middlewares)
);

可以看到中间件是在createStore参数里调用的(在参数里运行函数,导致传递给createStore的是中间件运行后返回的结果,从上面的中间件源码可以知道,返回的就是携带createStore参数的函数),现在我们可以createStore函数里看看他是怎么处理中间件返回的函数的。

redux的主要实现都是在createStore里实现的,所以我们主要看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.')
  }

  //other code
}

从我们调用createStore可以知道第一个参数是reducer,第二个参数就是中间件运行之后返回的携带createStore参数的函数。但是在上面的这段源码里,我们发现是preloadedState来接收这个携带createStore参数的函数,感觉不是很多,命名的'不好'。先继续往下看,wow, 是一个判断,他会判断preloadedState是不是一个函数,第三个参数enhancer是不是未定义;如果preloadedState是函数,enhancer是未定义,那么就会把preloadedState赋值给enhancer,并且设置preloadedState是未定义。 这样就没有问题了,在这里,相当于第三个参数enhancer接收了携带createStore参数的函数。

然后第二个判断:

if (typeof enhancer !== 'undefined') {
  if (typeof enhancer !== 'function') {
    throw new Error('Expected the enhancer to be a function.')
  }

  return enhancer(createStore)(reducer, preloadedState)
}

他会去运行这个enhancer。这个enhancer是什么?就是我们说的携带createStore的函数。

有意思的是,这个enhancer直接在这里运行了,并且采用了createStore作为参数(这个createStore就是函数呀)。 我们再来看看enhancer(createStore)返回的是啥:

return function (...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)
      }
      const chain = middlewares.map(middleware => middleware(middlewareAPI))
      dispatch = compose(...chain)(store.dispatch)

      return {
        ...store,
        dispatch
      }
    }

有意思,返回的是带有多个参数的函数。
上面的代码相当于:

enhancer(createStore) ~= function(...args) => function(reducer, preloadedState)

可以看到,上面的(...args)就是相当于(reducer, preloadedState)

那么我们再来看看上面的function(...args), 额, 直接在第一行就再次调用创建store,这样不会陷入无限循环吗?不会,因为有参数判断,在createStore的原方法里不会再执行enhancer; 所以我们可以发现,在有中间件的时候,真正的执行createStore是在中间件里去执行的,并且携带的参数是reducer, preloadedState

所以上面第一行创建了个store对象,他返回的属性有:

{
    dispatch,
    subscribe,
    getState,
    replaceReducer,
    [$$observable]: observable
  }

然后新建了个指向dispatch变量的匿名函数,这个函数在调用的时候抛出异常告诉你不可以在构造中间件时调用dispatch

Dispatching while constructing your middleware is not allowed. Other middleware would not be applied to this dispatch.

接下来会创建一个middlewareAPI对象:

const middlewareAPI = {
    getState: store.getState, //获取store里的state
    dispatch: (...args) => dispatch(...args) // 调用`dispatch`的时候会抛错,如果在组合中间件之前调用,下面会说
}

一开始我以为是在调用的时候就会报错,可是发现这个对象里的dispatch携带参数,如果只是单纯抛错,完全可以不需要传递参数,然后向下看下去才看到其中的奥妙。

然后就是对中间件集合middlewares(数组)进行操作:

const chain = middlewares.map(middleware => middleware(middlewareAPI)) //返回了新的集合,对应的每个中间件调用的结果

然后就是组合这些中间件了, 这里对高阶函数不熟的,可以看下柯里化函数和函数组合::

// china是上面返回的中间件的结果
dispatch = compose(...chain)(store.dispatch)

可以看到这个代码,组合了中间件, 使用compose这个高阶函数来处理的。我们看下这个高阶函数:

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

  if (funcs.length === 1) {
    return funcs[0]
  }

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

上面的代码比较有意思:

  • 第一个判断
    如果没有中间件作为参数传递,那么直接返回一个函数,接收对应的参数并且返回这个参数。
  • 第二个判断
    如果如果这个中间件参数只有一个,那么直接返回这个中间件函数
  • 最后一步
    那就是多个中间件传递进来的时候,他会借用reduce方法组合(这个放在后面), 会有个...args参数,就是(store.dispatch),等下回说到。
    可能你对reduce不是很熟,可以简单的看下他干了什么事:
['1', 2, 3, 'n'].reduce((a, b) => console.log('a is',a , 'b is', b)) // 这样你就会发现这个方法在这里的作用

其实从注释里也可以知道:

* @param {...Function} funcs The functions to compose.
* @returns {Function} A function obtained by composing the argument functions
* from right to left. For example, compose(f, g, h) is identical to doing
* (...args) => f(g(h(...args))).

把这些中间件都执行到dispatch.

再回到上面看compose的返回:

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

我们再看看中间件调用compose的地方:

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

从这个地方再配合看compose(...chain) => result的这个result.

  • 第一个判断
    返回的是(arg) => arg就是相当于 result(arg) => arg, 果然,直接返回这个store.dispatch
  • 第二个判断
    返回的是唯一的一个中间件result. 然后中间件直接调用store.dispatch作为参数。
  • 最后一个
    这个返回的是一个函数,看起来像这样:
(...args) => a(b(...args))

这样就相当于result(args) => a(b(...args)),这样就保证每个中间件都会用到dispatch,并且最终返回这个被扩展过的dispatch.

然后可以看到中间件函数返回了对象:

{
  ...store,
  dispatch
}

这个dispatch就是被处理过的dispatch

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,258评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,335评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,225评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,126评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,140评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,098评论 1 295
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,018评论 3 417
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,857评论 0 273
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,298评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,518评论 2 332
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,678评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,400评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,993评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,638评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,801评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,661评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,558评论 2 352

推荐阅读更多精彩内容