redux源码解析

背景

从flux再到mobx,最后到redux。到目前为止,本人也算展转了好几个数据流管理工具了。通过不断的实践和思考,我发现对于中大型项目来讲,redux无疑是当下综合考量下最优秀的一个。因此我决定深刻它的源码,一探究竟。javascript

探究主题

redux的源码很少,可是都是浓缩的精华,能够说字字珠玑。 咱们能够简单地将redux的源码划分为三部分。html

  • redux这个类库/包的入口。这里就对应createStore.js的源码。
  • state的combine机制。这里对应于combineReducers.js。
  • 中间件的组合原理。这里对应于applyMiddleware.js和compose.js。

之因此没有提到utils文件夹和bindActionCreators.js,这是由于他们都是属于工具类的代码,不涉及到redux的核心功能,故略过不表。java

与以上所提到的三部分代码相呼应的三个主题是:react

  • createStore.js到底作了什么?
  • redux是如何将各个小的reducer combine上来,组建总的,新的state树呢?
  • 中间件机制是如何工做的呢?它的原理又是什么呢?

下面,让咱们深刻到源码中,围绕着这三个主题,来揭开redux的神秘面纱吧!git

1、createStore.js到底作了什么?

为了易于理解,咱们不妨使用面向对象的思惟去表达。 一句话,createStore就是Store类的“构造函数”。github

Store类主要有私有属性:express

  • currentReducer
  • currentState
  • currentListeners
  • isDispatching

公有方法:编程

  • dispatch
  • subscribe
  • getState
  • replaceReducer

而在这几个公有方法里面,getStatereplaceReducer无疑就是特权方法。由于经过getState能够读取私有属性currentState的值,而经过replaceReducer能够对私有属性currentReducer进行写操做。虽然咱们是使用普通的函数调用来使用createStore,可是它本质上能够看做是一个构造函数调用。经过这个构造函数调用,咱们获得的是Store类的一个实例。这个实例有哪些方法,咱们经过源码能够一目了然:redux

return {
    dispatch,
    subscribe,
    getState,
    replaceReducer,
    [$$observable]: observable
  }
复制代码

官方推荐咱们全局只使用一个Store类的实例,咱们能够将此看做是单例模式的践行。 其实,咱们也能够换取另一个角度去解读createStore.js的源码:模块模式。 为何可行呢?咱们能够看看《你不知道的javascript》一文中如何定义javascript中的模块模式的实现:api

  • 必须有外部的嵌套函数,该函数必须至少被调用一次(每次调用都会建立一个新的模块实例)。
  • 嵌套函数必须返回至少一个内部函数,这样内部函数才能在私有做用域中造成闭包,而且能够访问或者修改私有的状态。

咱们再来看看createStore.js的源码的骨架。首先,最外层的就是一个叫createStore的函数,而且它返回了一个字面量对象。这个字面量对象的属性值保存着一些被嵌套函数(内部函数)的引用。因此咱们能够说createStore函数实际上是返回了至少一个内部函数。咱们能够经过返回的内部函数来访问模块的私有状态(currentReducer,currentState)。而咱们在使用的时候,也是至少会调用一次createStore函数。综上所述,咱们能够把createStore.js的源码看做是一次模块模式的实现。

export default function createStore(reducer, preloadedState, enhancer) {
    //......
    //......
    return {
    dispatch,
    subscribe,
    getState,
    replaceReducer,
    [$$observable]: observable
  }
}
复制代码

其实不管是从类模式仍是模块模式上解读,createStore.js本质上就是基于闭包的原理实现了数据的封装-经过私有变量来防止了全局污染和命名冲突,同时还容许经过特定的方法来访问特定的私有变量。

2、redux是如何将各个小的reducer combine上来?state树的初始化如何进行的?后面又是如何更新的?

首先,咱们得搞清楚什么是reducer?接触过函数式编程的人都知道,reducer是很纯粹的函数式编程概念。简单来讲,它就是一个函数。只不过不一样于普通函数(实现特定的功能,可重复使用的代码块)这个概念,它还多了一层概念-“纯”。对,reducer必须是纯函数。所谓的“纯函数”是指,一样的输入会产生一样的输出。好比下面的add函数就是一个纯函数:

function add(x,y){
    return x+y;
}
复制代码

由于你不管第几回调用add(1,2),结果都是返回3。好,解释到这里就差很少了。由于要深究函数式编程以及它的一些概念,那是说上几天几夜都说不完的。在这里,咱们只须要明白,reducer是一个有返回值的函数,而且是纯函数便可。为何这么说呢?由于整个应用的state树正是由每一个reducer返回的值所组成的。咱们能够说reducer的返回值是组成整颗state树的基本单元。而咱们这个小节里面要探究的combine机制本质上就是关于如何组织state树-一个关于state树的数据结构问题。 在深刻探究combine机制及其原理以前,咱们先来探讨一下几个关于“闭包”的概念。

什么是“闭包”?固然,这里的闭包是指咱们常说的函数闭包(除了函数闭包,还有对象闭包)。关于这个问题的答案可谓仁者见仁,智者见智。在这里,我就不做过多的探讨,只是说一下我对闭包的理解就好。由于我正是靠着这几个对闭包的理解来解读reducer的combine机制及其原理的。我对闭包的理解主要是环绕在如下概念的:

  1. 造成一个能够观察获得的闭包(代码书写期)
  2. 产生某个闭包(代码运行期)
  3. 进入某个闭包(代码运行期)

首先,说说“造成一个能够观察获得的闭包”。要想造成一个能够观察获得的闭包,咱们必须知足两个前提条件:

  • (词法)做用域嵌套。
  • 内层做用域对外层做用域的标识符进行了访问/引用。

其次,说说“产生某个闭包”。正如周爱民所说的,不少人没有把闭包说清楚,那是由于他们忽略了一个事实:闭包是一个运行时的概念。对此,我十分认同。那么怎样才能产生一个闭包呢?这个问题的答案是在前面所说的那个前提条件再加上一个条件:

  • 经过调用嵌套函数,将被嵌套的函数实例引用传递到外层做用域(嵌套函数所在的词法做用域)以外。

咱们能够简单地总结一下:当咱们对一个在内部实现中造成了一个能够观察获得的闭包的函数进行调用操做的时候,咱们就说“产生某个闭包”。

最后,说说“进入某个闭包”。同理,“进入某个闭包”跟“产生某个闭包”同样,都是一个运行时的概念,他们都是在函数被调用的时候所发生的。只不过,“进入某个闭包”所对应的函数调用是不同而已。当咱们对传递到嵌套做用域以外的被嵌套函数进行调用操做的时候,咱们能够说“进入某个闭包”。

好了,通过一大堆的概念介绍后,你可能会有点云里雾里的,不知道我在说什么。下面咱们结合combineReducers.js的源码和实际的使用例子来娓娓道来。

二话不说,咱们来看看combineReducers.js的总体骨架。

export default function combineReducers(reducers) {
  const finalReducers = {}
  //省略了不少代码
  const finalReducerKeys = Object.keys(finalReducers)
  //省略了不少代码
  
  return function combination(state = {}, action) {
    let hasChanged = false
    const nextState = {}
     for (let i = 0; i < finalReducerKeys.length; i++) {
       const key = finalReducerKeys[i]
       const reducer = finalReducers[key]
       //省略了不少代码
    }
    return hasChanged ? nextState : state
  }
}
复制代码

在这里,咱们能够看到combineReducers的词法做用域里面嵌套着combination的词法做用域,而且combination的词法做用域持有着它的上层做用域的好几个变量。这里,只是摘取了finalReducers和finalReducerKeys。因此,咱们说,在combineReducers函数的内部,有一个能够被观察获得的闭包。而这事是redux类库帮咱们作了。

const todoReducer=combineReducers({
    list:listReducer,
    visibility:visibilityReducer
})
复制代码

当咱们像上面那样调用combineReducers,在运行期,这里就会产生一个闭包

const todoReducer=combineReducers({
    list:listReducer,
    visibility:visibilityReducer
})
const prevState = {
    list:[],
    visibility:'showAll'
}
const currAction = { 
    type:"ADD_TODO",
    payload:{
        id:1,
        text:'23:00 准时睡觉',
        isFinished: false
    }
}
todoReducer(prevState,currAction)
复制代码

当咱们像上面那样调用combineReducers()返回的todoReducer,在运行期,咱们就会开始进入一个闭包

这里顺便提一下,与combineReducers()函数的实现同样。createStore()函数内部的实现也有一个“能够观察获得的闭包”,在这里就不赘言了。

其实提到闭包,这里还得提到函数实例。函数狭义上来说应该是指代码书写期的脚本代码,是一个静态概念。而函数实例偏偏相反,是一个函数在代码运行期的概念。函数实例与闭包存在对应关系。一个函数实例能够对应一个闭包,多个函数实例能够共享同一个闭包。因此,闭合也是一个运行期的概念。明白这一点,对于理解我后面所说相当重要。

在这个小节,咱们提出的疑问是“redux是如何将各个小的reducer combine上来,组建总的state树?”。答案是经过combineReducers闭包的层层嵌套。经过查看源码咱们知道,调用combineReducers,咱们能够获得一个combination函数的实例,而这个函数实例有一个对应的闭包。也就是说,在代码书写期,redux类库[造成了一个能够观察获得的闭包]。咱们的js代码初始加载和执行的时候,经过调用combineReducers()函数,咱们获得了一个函数实例,咱们把这个函数实例的引用保存在调用combineReducers函数时传进去的对象属性上。正如上面所说每一个函数实例都会有一个与之对应的闭包。所以咱们理解为,咱们经过调用combineReducers(),事先(注意:这个“事先”是相对代码正式能够跟用户交互阶段而言)生成了一个闭包,等待进入。由于combineReducers的使用是层层嵌套的,因此这里产生了一个闭包链。既然闭包链已经准备好了,那么咱们何时进入这条闭包链呢?也许你会有个疑问:“进入闭包有什么用?”。由于闭包的本质是将数据保存在内存当中,因此这里的答案就是:“进入闭包,是为了访问该闭包在产生时保存在内存当中的数据”。在redux中,state被设计为一个树状结构,而咱们的combine工做又是自底向上来进行的。能够这么讲,在自底向上的combine工做中,当咱们完成最后一次调用combineReducers时,咱们的闭包链已经准备稳当了,等待咱们的进入。现实开发中,因为都采用了模块化的开发方式,combine工做的实现代码都是分散在各个不一样的文件里面,最后是经过引用传递的方式来汇总在一块。假如一开始咱们把它们写在一块儿,那么咱们就很容易看到combine工做的全貌。就以下代码所演示的那样:

const finalReducer = combineReducers({
    page1:combineReducers({
        counter:counterReducer,
        todo:combineReducers({
            list:listReducer,
            visibility:visibilityReducer
        })
    })
})

复制代码

咱们能够说当代码执行到[给finalReducer变量赋值]的时候,咱们的闭包链已经准备好了,等待咱们的进入。 接下来,咱们不由问:“那到底何时进入呢?”。答案是:“finalReducer被调用的时候,咱们就开始进入这个闭包链了。”。那么finalReducer何时,在哪里被调用呢?你们能够回想如下,咱们最后一次消费finalReducer是否是就是调用createStore()时做为第一个参数传入给它?是的。因而乎,咱们就来到createStore.js的源码中来看看:

import $$observable from 'symbol-observable'

import ActionTypes from './utils/actionTypes'
import isPlainObject from './utils/isPlainObject'

/** * Creates a Redux store that holds the state tree. * The only way to change the data in the store is to call `dispatch()` on it. * * There should only be a single store in your app. To specify how different * parts of the state tree respond to actions, you may combine several reducers * into a single reducer function by using `combineReducers`. * * @param {Function} reducer A function that returns the next state tree, given * the current state tree and the action to handle. * * @param {any} [preloadedState] The initial state. You may optionally specify it * to hydrate the state from the server in universal apps, or to restore a * previously serialized user session. * If you use `combineReducers` to produce the root reducer function, this must be * an object with the same shape as `combineReducers` keys. * * @param {Function} [enhancer] The store enhancer. You may optionally specify it * to enhance the store with third-party capabilities such as middleware, * time travel, persistence, etc. The only store enhancer that ships with Redux * is `applyMiddleware()`. * * @returns {Store} A Redux store that lets you read the state, dispatch actions * and subscribe to changes. */
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.')
  }

  let currentReducer = reducer
  let currentState = preloadedState
  let currentListeners = []
  let nextListeners = currentListeners
  let isDispatching = false

  function ensureCanMutateNextListeners() {
    if (nextListeners === currentListeners) {
      nextListeners = currentListeners.slice()
    }
  }

  /** * Reads the state tree managed by the store. * * @returns {any} The current state tree of your application. */
  function getState() {
    if (isDispatching) {
      throw new Error(
        'You may not call store.getState() while the reducer is executing. ' +
          'The reducer has already received the state as an argument. ' +
          'Pass it down from the top reducer instead of reading it from the store.'
      )
    }

    return currentState
  }

  /** * Adds a change listener. It will be called any time an action is dispatched, * and some part of the state tree may potentially have changed. You may then * call `getState()` to read the current state tree inside the callback. * * You may call `dispatch()` from a change listener, with the following * caveats: * * 1. The subscriptions are snapshotted just before every `dispatch()` call. * If you subscribe or unsubscribe while the listeners are being invoked, this * will not have any effect on the `dispatch()` that is currently in progress. * However, the next `dispatch()` call, whether nested or not, will use a more * recent snapshot of the subscription list. * * 2. The listener should not expect to see all state changes, as the state * might have been updated multiple times during a nested `dispatch()` before * the listener is called. It is, however, guaranteed that all subscribers * registered before the `dispatch()` started will be called with the latest * state by the time it exits. * * @param {Function} listener A callback to be invoked on every dispatch. * @returns {Function} A function to remove this change listener. */
  function subscribe(listener) {
    if (typeof listener !== 'function') {
      throw new Error('Expected the listener to be a function.')
    }

    if (isDispatching) {
      throw new Error(
        'You may not call store.subscribe() while the reducer is executing. ' +
          'If you would like to be notified after the store has been updated, subscribe from a ' +
          'component and invoke store.getState() in the callback to access the latest state. ' +
          'See http://redux.js.org/docs/api/Store.html#subscribe for more details.'
      )
    }

    let isSubscribed = true

    ensureCanMutateNextListeners()
    nextListeners.push(listener)

    return function unsubscribe() {
      if (!isSubscribed) {
        return
      }

      if (isDispatching) {
        throw new Error(
          'You may not unsubscribe from a store listener while the reducer is executing. ' +
            'See http://redux.js.org/docs/api/Store.html#subscribe for more details.'
        )
      }

      isSubscribed = false

      ensureCanMutateNextListeners()
      const index = nextListeners.indexOf(listener)
      nextListeners.splice(index, 1)
    }
  }

  /** * Dispatches an action. It is the only way to trigger a state change. * * The `reducer` function, used to create the store, will be called with the * current state tree and the given `action`. Its return value will * be considered the **next** state of the tree, and the change listeners * will be notified. * * The base implementation only supports plain object actions. If you want to * dispatch a Promise, an Observable, a thunk, or something else, you need to * wrap your store creating function into the corresponding middleware. For * example, see the documentation for the `redux-thunk` package. Even the * middleware will eventually dispatch plain object actions using this method. * * @param {Object} action A plain object representing “what changed”. It is * a good idea to keep actions serializable so you can record and replay user * sessions, or use the time travelling `redux-devtools`. An action must have * a `type` property which may not be `undefined`. It is a good idea to use * string constants for action types. * * @returns {Object} For convenience, the same action object you dispatched. * * Note that, if you use a custom middleware, it may wrap `dispatch()` to * return something else (for example, a Promise you can await). */
  function dispatch(action) {
    if (!isPlainObject(action)) {
      throw new Error(
        'Actions must be plain objects. ' +
          'Use custom middleware for async actions.'
      )
    }

    if (typeof action.type === 'undefined') {
      throw new Error(
        'Actions may not have an undefined "type" property. ' +
          'Have you misspelled a constant?'
      )
    }

    if (isDispatching) {
      throw new Error('Reducers may not dispatch actions.')
    }

    try {
      isDispatching = true
      currentState = currentReducer(currentState, action)
    } finally {
      isDispatching = false
    }

    const listeners = (currentListeners = nextListeners)
    for (let i = 0; i < listeners.length; i++) {
      const listener = listeners[i]
      listener()
    }

    return action
  }

  /** * Replaces the reducer currently used by the store to calculate the state. * * You might need this if your app implements code splitting and you want to * load some of the reducers dynamically. You might also need this if you * implement a hot reloading mechanism for Redux. * * @param {Function} nextReducer The reducer for the store to use instead. * @returns {void} */
  function replaceReducer(nextReducer) {
    if (typeof nextReducer !== 'function') {
      throw new Error('Expected the nextReducer to be a function.')
    }

    currentReducer = nextReducer
    dispatch({ type: ActionTypes.REPLACE })
  }

  /** * Interoperability point for observable/reactive libraries. * @returns {observable} A minimal observable of state changes. * For more information, see the observable proposal: * https://github.com/tc39/proposal-observable */
  function observable() {
    const outerSubscribe = subscribe
    return {
      /** * The minimal observable subscription method. * @param {Object} observer Any object that can be used as an observer. * The observer object should have a `next` method. * @returns {subscription} An object with an `unsubscribe` method that can * be used to unsubscribe the observable from the store, and prevent further * emission of values from the observable. */
      subscribe(observer) {
        if (typeof observer !== 'object') {
          throw new TypeError('Expected the observer to be an object.')
        }

        function observeState() {
          if (observer.next) {
            observer.next(getState())
          }
        }

        observeState()
        const unsubscribe = outerSubscribe(observeState)
        return { unsubscribe }
      },

      [$$observable]() {
        return this
      }
    }
  }

  // When a store is created, an "INIT" action is dispatched so that every
  // reducer returns their initial state. This effectively populates
  // the initial state tree.
  dispatch({ type: ActionTypes.INIT })

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

复制代码

果不其然,在dispatch方法的内部实现,咱们看到了咱们传入的finalReducer被调用了(咱们缩减了部分代码来看):

export default function createStore(reducer, preloadedState, enhancer) {
    ...... // other code above
     let currentReducer = reducer
     ...... // other code above
    function dispatch(action) {
     ...... // other code above

    try {
      isDispatching = true
      // 注意看,就是在这里调用了咱们传入的“finalReducer”
      currentState = currentReducer(currentState, action)
    } finally {
      isDispatching = false
    }
    ...... // other code rest
  }
}
复制代码

也就是说,咱们在调用dispatch(action)的时候(准确地来讲,第一次调用dispatch的并非咱们,而是redux类库本身),咱们就开始进入早就准备好的combination闭包链了。 到这里咱们算是回答了这个小节咱们本身给本身提出的第一个小问题:“redux是如何将各个小的reducer combine上来?”。下面,咱们一块儿来总结如下:

  1. redux类库在代码书写期就[造成了一个能够观察获得的闭包]-combination闭包。
  2. (redux类库的)用户本身经过自底向上地调用combineReducers()来产生了一个层层嵌套的闭包链。
  3. 最终,在咱们以一个action去调用dispatch()方法的时候,咱们就会进入了这个闭包链。

既然combine工做的原理咱们已经搞清楚了,那么第二个小问题和第三个小问题就迎刃而开了。

首先咱们来看看createStore的函数签名:

createStore(finalreducer, preloadedState, enhancer) => store实例
复制代码

state树的初始值取决于咱们调用createStore时的传参状况。

  • 假如,咱们有传递preloadedState这个实参,那么这个实参将做为state树的初始值。
  • 假如,咱们没有传递preloadedState这个实参,那么state树的初始化将会是由redux类库本身来完成。具体地说,redux在内部经过dispatch一个叫{ type: ActionTypes.INIT }的action来完成了state树的初始化。在这里,咱们得提到reducer的初始值。reducer的初始值指的是reducer在没有匹配到action.type的状况下返回的值。一个纯正的reducer,咱们通常会这么写:
function counterReducer(state = 0, action){
    switch (action.type) {
        case "INCREMENT":
            return state + 1;
        case "DECREMENT":
            return state - 1;
        default:
            return state;
    }
}
复制代码

在上面的代码中,若是当前dispatch的action在counterReducer里面没有找到匹配的action.type,那么就会走default分支。而default分支的返回值又等同具备默认值为0的形参state。因此,咱们能够说这个reducer的初始值为0。注意,形参state的默认值并不必定就是reducer的初始值。考虑下面的写法(这种写法是不被推荐的):

function counterReducer(state = 0, action){
    switch (action.type) {
        case "INCREMENT":
            return state + 1;
        case "DECREMENT":
            return state - 1;
        default:
            return1;
    }
}
复制代码

在switch语句的default条件分支里面,咱们是return -1,而不是return state。在这种状况下,reducer的初始值是-1,而不是0 。因此,咱们不能说reducer的初始值是等同于state形参的默认值了。

也许你会问:“为何dispatch 一个叫{ type: ActionTypes.INIT }action就能将各个reducer的初始值收集上来呢?”。咱们先来看看ActionTypes.INIT究竟是什么。

utils/actionTypes.js的源码:

/** * These are private action types reserved by Redux. * For any unknown actions, you must return the current state. * If the current state is undefined, you must return the initial state. * Do not reference these action types directly in your code. */
const ActionTypes = {
  INIT:
    '@@redux/INIT' +
    Math.random()
      .toString(36)
      .substring(7)
      .split('')
      .join('.'),
  REPLACE:
    '@@redux/REPLACE' +
    Math.random()
      .toString(36)
      .substring(7)
      .split('')
      .join('.')
}
复制代码

能够看到,ActionTypes.INIT的值就是一个随机字符串。也就是说,在redux类库内部(createStore函数内部实现里面)咱们dispatch了一个type值为随机字符串的action。这对于用户本身编写的reducer来讲,99.99%都不可能找到与之匹配的action.type 。因此,ActionTypes.INIT这个action最终都会进入default条件分支。也就是说,做为响应,各个reducer最终会返回本身的初始值。而决定state树某个子树结构的字面量对象,咱们早就在产生闭包时存在在内存当中了。而后,字面量对象(表明着结构) + 新计算出来的new state(表明着数据) = 子state树。最后,经过组合层层子state树,咱们就初始化了一颗完整的state树了。

回答完第二个问题后,那么接着回答第三个问题:“state树是如何更新的?”。其实,state树跟经过dispatch一个type为ActionTypes.INIT的action来初始化state树的原理是同样的。它们都是经过给全部最基本的reducer传入一个该reducer负责管理的节点的previous state和当前要广播的action来产出一个最新的值,也就是说: previousState + action => newState。不一样于用于state树初始化的action,后续用于更新state树的action通常会带有payload数据,而且会在某个reducer里面匹配到对应action.type,从而计算出最新的state值。从这里咱们也能够看出了,reducer的本质是计算。而计算也正是计算机的本质。

最后,咱们经过对比combineReducers函数的调用代码结构和其生成的state树结构来加深对combine机制的印象:

// 写在一块的combineReducers函数调用
const finalReducer = combineReducers({
    page1:combineReducers({
        counter:counterReducer,// 初始值为0
        todo:combineReducers({
            list:listReducer,// 初始值为[]
            visibility:visibilityReducer// 初始值为"showAll"
        })
    })
})

// 与之对应的state树的初始值
{
    page1:{
        counter:0,
        todo:{
            list:[],
            visibility:"showAll"
        }
    }
}
复制代码

3、中间件运行机制与原理是什么呢?

在探讨redux中间件机制以前,咱们不妨来回答一下中间件是什么?答曰:“redux的中间件其实就是一个函数。

更确切地讲是一个包了三层的函数,形如这样:

const middleware = function (store) {

            return function (next) {

                return function (action) {
                    // maybe some code here ...
                    next(action)
                    // maybe some code here ...
                }
            }
        }
复制代码

既然咱们知道了redux的中间件长什么样,那么咱们不由问:“为何redux的中间件长这样能有用呢?整个中间件的运行机制又是怎样的呢?”

咱们不妨回顾一下,咱们平时使用中间件的大体流程:

  1. 写好一个中间件;

  2. 注册中间件,以下:

import Redux from 'redux';

const enhancer = Redux.applyMiddleware(middleware1,middleware2)
复制代码
  1. 把enhancer传入redux的createStore方法中,以下:
import Redux from 'redux';

let store = Redux.createStore(counterReducer,enhance);
复制代码

其实要搞清楚中间件的运行机制,无非就是探索咱们写的这个包了三层的函数是如何被消费(调用)。咱们写的中间件首次被传入了applyMiddleware方法,那咱们来瞧瞧这个方法的真面目吧。源文件applyMiddleware.js的代码以下:

import compose from './compose'

/**
 * Creates a store enhancer that applies middleware to the dispatch method
 * of the Redux store. This is handy for a variety of tasks, such as expressing
 * asynchronous actions in a concise manner, or logging every action payload.
 *
 * See `redux-thunk` package as an example of the Redux middleware.
 *
 * Because middleware is potentially asynchronous, this should be the first
 * store enhancer in the composition chain.
 *
 * Note that each middleware will be given the `dispatch` and `getState` functions
 * as named arguments.
 *
 * @param {...Function} middlewares The middleware chain to be applied.
 * @returns {Function} A store enhancer applying the middleware.
 */
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.`
      )
    }
    let chain = []

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

    chain = middlewares.map(middleware => middleware(middlewareAPI)) // 这里剥洋葱模型的第一层
    dispatch = compose(...chain)(store.dispatch) //  第二个函数调用剥洋葱模型的第二层

    return {
      ...store,
      dispatch
    }
  }
  //return enhancer(createStore)(reducer, preloadedState)
}

复制代码

又是function返回function,redux.js处处可见闭包,可见做者对闭包的应用已经到随心应手,炉火纯青的地步了。就是上面几个简短却不简单的代码,实现了redux的中间件机制,可见redux源代码的凝练程度可见一斑。

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) // 反客为主的代码
  }
  // 剩余的其余代码
  }
复制代码

咱们手动传入createStore方法的reducer, preloadedState等参数绕了一圈,最终仍是原样传递到applyMiddleware方法的内部createStore。这就是这行代码:

const store = createStore(...args)
复制代码

顺便提一下,在applyMiddleware方法里面的const store = createStore(...args)所建立的store与咱们日常调用redux.createStore()所产生的store是没什么区别的。

来到这里,咱们能够讲applyMiddleware方法里面的middlewareAPI对象所引用的getState和dispatch都是未经改造的,原生的方法。而咱们写中间件的过程就是消费这两个方法(主要是加强dispatch,使用getState从store中获取新旧state)的过程。

正如上面所说的,咱们的中间件其实就是一个包了三层的函数。借用业界的说法,这是一个洋葱模型。咱们中间件的核心代码通常都是写在了最里面那一层。那下面咱们来看看咱们传给redux类库的中间件这个洋葱,是如何一层一层地被拨开的呢?而这就是中间件运行机制之所在。

剥洋葱的核心代码只有两行代码(在applyMiddleware.js):

//  ......

chain = middlewares.map(middleware => middleware(middlewareAPI)) // 这里剥洋葱模型的第一层
dispatch = compose(...chain)(store.dispatch) //  第二个函数调用剥洋葱模型的第二层

// .......
复制代码

还记得咱们传给applyMiddleware方法的是一个中间件数组吗?经过对遍历中间件数组,以middlewareAPI入参,咱们剥开了洋葱模型的第一层。也便是说chain数组中的每个中间件都是这样的:

function (next) {
    return function (action) {
        // maybe some code here ...
        next(action)
        // maybe some code here ...
    }
}
复制代码

剥开洋葱模型的第一层的同时,redux往咱们的中间件注入了一个简化版的store对象(只有getState方法和dispatch方法),仅此而已。而剥开洋葱模型的第二层,才是整个中间件运行机制的灵魂所在。咱们目光往下移,只见“寥寥数语”:

dispatch = compose(...chain)(store.dispatch) //  第二个函数调用剥洋葱模型的第二层
复制代码

是的,compose方法才是核心所在。咱们不防来看看compose方法源码:

/**
 * Composes single-argument functions from right to left. The rightmost
 * function can take multiple arguments as it provides the signature for
 * the resulting composite function.
 *
 * @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))).
 */

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

  if (funcs.length === 1) {
    return funcs[0]
  }
  // 此处的a和b是指中间件的第二层函数
  return funcs.reduce((a, b) => (...args) => a(b(...args)));
  
  // 为了看清楚嵌套层数,咱们转成ES5语法来看看
  // return funcs.reduce(function(a,b){
  //   return function(...args){
  //     return a(b(...args));
  //   }
  // })
}

复制代码

compose方法的实现跟任何一个函数式编程范式里面的“compose”概念的实现没有多大的差异(又是函数嵌套 + return,哎,心累)。要想理解这行代码,我的以为须要用到几个概念:

  • 函数能够被当成值来传递
  • 延迟求值
  • 闭包

咱们使用了数组的reduce API把函数看成值来遍历了一遍,每一个函数都被闭包在最后返回的那个函数的做用域里面。虽然表面上看到了对每一个函数都使用了函数调用操做符,可是实际上每一个函数都延迟执行了。这段代码须要在脑海里面好好品味一下。不过,为了快速理解后面的代码,咱们的不防把它对应到这样的心智模型中去:

compose(f,g,h) === (...args) => f(g(h(...args)))
复制代码

一旦理解了compose的原理,咱们就会知道咱们中间件的洋葱模型的第二层是在compose(...chain)(store.dispatch)的最后一个函数调用发生时剥开的。而在剥开的时候,咱们每个中间件的第二层函数都会被注入一个通过后者中间件(按照注册中间时的顺序来算)加强后的dispatch方法。f(g(h(...args)))调用最后返回的是第一个中间件的最里层的那个函数,以下:

function (action) {
    //  ...
    next(action)
    //  ...
}
复制代码

也便是说,用户调用的最终是加强(通过各个中间件魔改)后的dispatch方法。由于这个被中间件加强后的dispatch关联着一条闭包链(这个加强后的dispatch相似于上面一章节所提到的totalReducer,它也是关联着一个闭包链),因此对于一个严格使用redux来管理数据流的应用,咱们能够这么说:中间件核心代码(第三层函数)执行的导火索就是紧紧地掌握在用户的手中。

讲到这里,咱们已经基本上摸清了中间件机制运行的原理了。下面,总结一下:

  1. 调用applyMiddleware方法的时候,redux会把咱们的中间件剥去两层外衣。剥开第一层的时候,往里面注入简化版的store实例;剥开第二层的时候,往里面注入上一个中间件(按照注册中间时的顺序来算)的最里层的函数。以此类推,中间件数组最后一个中间件的第二层被剥开的时候,注入的是原生的dispatch方法。
  2. 用户调用的dispatch方法是通过全部中间件加强后的dispatch方法,已然不是原生的dispatch方法了。
  3. 用户调用dispatch方法的时候,程序会进入一个环环相扣的闭包链中。也便是说,用户的每dispatch一次action,咱们注册的全部的中间件的第三层函数都会被执行一遍。

上面所提原理的心智模型图大概以下:

假如咱们已经理解了中间件的运行机制与原理,咱们不防经过自问自答的方式来巩固一下咱们的理解。咱们将会问本身三个问题:

1.不一样的中间件注册顺序对程序的执行结果有影响吗?若是有,为何有这样的影响?

答曰:有影响的。由于中间件的核心代码是写在第三层函数里面的,因此当咱们在讨论中间件的执行顺序时,通常是指中间件第三层函数的被执行的顺序。正如上面探索中间件的运行机制所指出的,中间件第三层函数的执行顺序与中间件的注册顺序是一致的,都是从左到右。因此,不一样的注册顺序也就意味着不一样的中间件调用顺序。形成这种影响的缘由我能想到的是如下两种场景:

  1. 后一个中间件对前一个中间件产生依赖。举个例子,假如某个中间件B须要依赖上一个中间件A对action进行添加的某个payload属性来判断是否执行某些逻辑,而在实际注册的时候,你却把中间件B放在所依赖中间件A的前面,那么这个中间件B的那段判断逻辑颇有可能永远都不会被执行。
  2. 是否有影响有时候取决与该中间件所想要实现的功能。举个例子,假如某个中间件想要实现的功能是统计dispatch一次action所耗费的时间,它的大概实现是这样的:
let startTimeStamp = 0
const timeLogger = function (store) {
    return function (next) {
        return function (action) {
            startTimeStamp = Date.now();
            next(action)
            console.log(`整个dispatch耗费的时间是:${Date.now() - startTimeStamp}毫秒`, )
        }
    }
}
复制代码

基因而为了实现这种功能的目的,那么这个timeLogger中间件就必须是放在中间件数组的第一位了。不然的话,统计出来的耗时就不是整个dispatch执行完所耗费的时间了。

2.咱们都知道中间件的核型代码会放在第三层函数那里,那若是我在第一层和第二层函数里面就开始写点代码(消费store这个实参)会是怎样呢?

在上面已经讨论过中间件运行机制片断中,咱们了解到,中间件的第一层和第二层函数都是在咱们调用applyMiddleware()时所执行的,也就是说不一样于中间件的第三层函数,第一层和第二层函数在应用的整个生命周期只会被执行一次。

let dispatch = () => {
  throw new Error(
    `Dispatching while constructing your middleware is not allowed. ` +
      `Other middleware would not be applied to this dispatch.`
  )
}
let chain = []

const middlewareAPI = {
  getState: store.getState,
  dispatch: (...args) => dispatch(...args)
}
复制代码

看这几行代码可知,若是咱们在第一层函数里面就调用dispatch方法,应该是会报错的。稍后咱们来验证一下。 而对于调用getState()而言,由于在chain = middlewares.map(middleware => middleware(middlewareAPI))这行代码以前,咱们已经建立了store实例,也便是说ActionTypes.INIT已经dispatch了,因此,state树的初始值已经计算出来的。这个时候,若是咱们调用middlewareAPI的getState方法,获得的应该也是初始状态的state树。咱们不防追想下去,在中间件的第二层函数里面消费middlewareAPI的这个两个方法,应该会获得一样的结果。由于,dispatch变量仍是指向同一函数引用,而应用中的第二个action也没有dispatch 出来,因此state树的值不变,仍是初始值。下面,咱们不防写个中间件来验证一下咱们的结论:

const testMiddleware = function (store) {
    
    store.dispatch(); // 通过验证,会报错:"Dispatching while constructing your middleware is not allowed. Other middleware would not be applied to this dispatch."
    
    console.log(store.getState()); // 在第一层,拿到的是整个state树的初始值(已验证)
    
    return function (next) {
    
        store.dispatch(); // 这里也不能写,也会包一样的错.
    
        console.log(store.getState()); // 在第二层,拿到的也是整个state树的初始值(已验证)

        return function (action) {
            next(action)
        }
    }
}
复制代码

3.在中间件的第三层函数里,写在next(action)以前的代码与写在以后的代码会有什么不一样吗?

正如咱们给出的步骤四的心智模型图,加强后的dispatch方法的代码执行流程是夹心饼干式的。对于每个中间件(第三层函数)而言,写在next(action)语句先后的语句分别是夹心饼干的上下层,中心层永远是通过后一个(这里按照中间件注册顺序来讲)中间件加强后的dispatch方法,也就是此时的next(action)。下面咱们不防用代码去验证一下:

const m1 = store=> next=> action=> {
    console.log('m1: before next(action)');
    next(action);
    console.log('m1: after next(action)');
}

const m2 = store=> next=> action=> {
    console.log('m2: before next(action)');
    next(action);
    console.log('m2: after next(action)');
}

// 而后再在原生的dispatch方法里面打个log
function dispatch(action) {
    console.log('origin dispatch');
    if (!isPlainObject(action)) {
      throw new Error(
        'Actions must be plain objects. ' +
          'Use custom middleware for async actions.'
      )
    }
    // ........
  }
复制代码

通过验证,打印的顺序以下:

m1: before next(action)
m2: before next(action)
origin dispatch
m1: after next(action)
m2: after next(action)
复制代码

是否是挺像夹心饼干的啊?

回到这个问题的自己,咱们不防也想一想第二章节里面获得过的结论:dispatch一次action,其实是一次计算,计算出state树的最新值。也就是说,只有原生的dispatch方法执行以后,咱们才能拿到最新的state。结合各个中间件与原生的dispatch方法的执行顺序之前后,这个问题的答案就呼之欲出了。

那就是:“在调用getState方法去获取state值的场景下,写在next(action)以前的代码与写在以后的代码是不一样的。这个不一样点在于写在next(action)以前的getState()拿到的是旧的state,而写在next(action)以后的getState()拿到的是新的state。”

中间件应用

既然咱们都探索了这么多,最后,咱们不妨写几个简单的中间件来巩固一下战果。

  1. 实现一个用于统计dispatch执行耗时的中间件。
let startTimeStamp = 0
const timeLogger = function (store) {
       return function (next) {
           return async function (action) {
               startTimeStamp = Date.now();
               await next(action)
               console.log(`整个dispatch花费的时间是:${Date.now() - startTimeStamp}毫秒`, )
           }
       }
   }
}
复制代码

注意,注册的时候,这个中间要始终放在第一位,个中理由上文已经解释过。

  1. 实现一个鉴权的中间件。
function auth(name) {
   return new Promise((reslove,reject)=> {
       setTimeout(() => {
           if(name === 'sam') {
               reslove(true);
           } else {
               reslove(false);
           }
       }, 1000);
   })
}
       
const authMiddleware = function(store){
   return function(next){
       return async function(action) {
           if (action.payload.isNeedToAuth) {
               const isAuthorized = await auth(action.payload.name);

               if(isAuthorized) {
                   next(action);
               } else {
                   alert('您没有此权限');
               }
           } else {
               next(action);
           }
           
       }
   }
}
复制代码

对于写中间件事而言,只要把中间件的运行机制的原理明白,剩下的无非就是如何“消费”store,nextaction等实参的事情。到这里,中间件的剖析就结束了。但愿你们能够结合实际的业务需求,发挥本身的聪明才智,早日写出有如《滕王阁序》中所提的“紫电青霜,王将军之武库”般的中间件库。

相关文章
相关标签/搜索