细品-Redux的设计

605 阅读22分钟

掌握真相,你才能获得自由

redux它很小(不到7k),但是却拥有丰富的(几百个)插件生态系统。这扩展性是多么好啊,才能拥有这么多的插件。本文将详细解读:每行redux及其中间件源码-(版本v4.0.5),不仅让你知道这句代码的含义,更会让你知道为什么这么写?。

1 介绍

redux是什么?

引用官方的话来说:redux是为js设计的一个可预测的状态管理应用。它能帮助您编写行为一致的应用,它可以运行在不同的环境(客户端,服务端,原生),并且非常容易测试。除此以外,它提供了很棒的开发人员体验,比如实时代码编辑与时间旅行调试器相结合。你可以在react或任何其他视图库中使用。

说白了,redux就是状态管理库-store,可以帮助我们保存一些状态-state,并提供一个方法-dispatch进行修改状态state。与此同时,我们也可以订阅-subscribe状态的变化,在每次状态改变(dispatch调用)后,来发送通知,以便处理相关逻辑。

dispatch.drawio.png

基本名词

  • store: 是一个容器,包含状态(state),修改状态的方法(dispatch())以及订阅状态变化的方法(subscribe())等。
  • state: 保存状态相关的数据,只能是普通对象,数组或者原始值,通常为普通对象。
  • action: 动作行为,它是一个普通对象,里面必须有type属性,用来描述当前的行为。
  • dispatch(): 派发器,派发一个动作(action),它是唯一修改状态(state)的方法。
  • reducer(): 处理器,参数为stateaction。通过action中不同的type,来返回新的state
  • subscribe(): 订阅器,接收一个回调函数,每次state改变,都会调用这个函数。

拓展名词

  • actionType: 指的就是action中的type。
  • actionCreator(): 生成action的函数。
  • 纯函数: 1.有相同的输入,必有相同的输出。2.不会有副作用,就是不会改变外面的状态。它是redux可预测功能的保证

三大核心

单一数据源: 是指应用的全局状态应保存在单一的store中(应用有且只有一个store)。state只读: state是不能直接被改变,改变它的唯一方式是调用dispatch()方法。用纯函数返回新的状态:这个纯函数指的就是reducer()

core.drawio.png

  • 单一数据源:可以方便调试检测
  • 只读的state:可以防止由于其他任何Modal层或View层,直接修改state,而导致难以复现的bug,从而保证所有内部状态的改变被集中管控,并且严格按照顺序执行
  • 再利用纯函数来返回新的状态,我们可以很方便的记录用户的操作,来实现撤销,重做,时间旅行等功能。

  1. 修改state只能通过dispatch方法,避免直接修改state的值。如果直接修改state某个属性,属性值虽然会变化,但这个变化并不会被订阅器监听到,这会引起难以复现的bug。
  2. 强制使用action来描述每次发生的变化,这会使我们清晰的知道应用程序中发生了什么。如果一个状态改变了,也可以知道它为什么改变。

中间件原理

中间件有什么作用呢?它可以扩展我们redux,例如通过中间件,我们可以dispatch一个异步action,可以帮助我们记录state状态变化,以及很容易实现时间旅行等众多功能。

那么redux中间件函数到底是改变的是什么?

答案:就是改造dispatch函数。

通过应用多个中间件函数,每个中间返回的dispatch()(即下图中next())被一层层包裹,像个洋葱一样,如下图:

middleware.drawio.png

如图所示,当我分别应用a、b、c三个中间件时,每次我们调用dispatch()方法,这些中间件函数会依次执行,其中最内层就是原始的dispatch()

带有中间件的任务流

m.d.drawio.png

2 createStore()

通过这个函数,我们可以创建一个store

createStore()可以传入三个参数:

  • reducer:(必传)是一个函数,根据不同的action,来返回新的state
  • preloadedState:(可选) 初始化state
  • enhancer: (可选)是增强函数,通常配合中间件使用。可以是applyMiddleware(中间件函数)

用法如下:

    const store = createStore(reducer, preloadedState, enhancer) // 第一种用法 
    const store = createStore(reducer, enhancer) // 第二种用法 或者,如果不在此函数中设置初始值(而是在reducer函数设置),则可以直接用 

reducer()可以接收2个参数:

  • state: (必传)是初始值
  • action: (必传)是一个对象,包含type(触发行为类型)属性,和与数据相关的属性。

用法如下:

    const initState = {
        num: 1,
    }
    const reducer = (state = initState, action) {
        const num = state.num
        switch (action.type) {
            case "add": return {...state, num: num + 1};
            case "minus": return {...state, num: num - 1};
            default: return state
        }
    }

createStore()函数主要功能:

  • 如果有中间件,将reducerstate传入中间件函数。
  • 初始化statereducer,以及初始化收集订阅函数的数组。
  • 返回四个方法,如下图:

image.png 源码如下:

    function createStore(reducer, preloadedState, enhancer) {
      // 如果第二参数,第三个参数都是函数
      // 或者第三个和第四个参数都是函数会抛出错误。
      if (
        (typeof preloadedState === 'function' && typeof enhancer === 'function') ||
        (typeof enhancer === 'function' && typeof arguments[3] === 'function')
      ) {
        throw new Error(
          'It looks like you are passing several store enhancers to ' +
            'createStore(). This is not supported. Instead, compose them ' +
            'together to a single function.'
        )
      }
      // 如果第二个参数是个函数,第三个参数为undefined,那么说明此时,调用方式为createStore(reducer, enhancer),此时preloadedState就是enhancer。
      // 所以需要将enhancer设置为preloadedState,而preloadedState为undefined
      if (typeof preloadedState === 'function' && typeof enhancer === 'undefined') {
        enhancer = preloadedState
        preloadedState = undefined
      }
      /**======================================================
        下面才是主要内容
      =======================================================**/ 
      // 如果传参的形式类似createStore(reducer, preloadedState, {})
      // 因为enhancer只能为函数,如果传入不是函数则报错。
      if (typeof enhancer !== 'undefined') {
        if (typeof enhancer !== 'function') {
          throw new Error('Expected the enhancer to be a function.')
        }
        // 这句会配合中间件讲解
        return enhancer(createStore)(reducer, preloadedState)
      }
      // 如果传入的reducer不是函数,也会抛出错误  
      if (typeof reducer !== 'function') {
        throw new Error('Expected the reducer to be a function.')
      }
      // 将传入reducer保存在currentReducer中
      let currentReducer = reducer
      // 将传入初始化state(preloadedState),保存在currentState
      let currentState = preloadedState
      // currentListeners和nextListeners保存订阅的回调函数队列
      let currentListeners = []
      let nextListeners = currentListeners
      // 判断是否正在执行dispatch方法
      let isDispatching = false
      // 定义以下几个方法
      function getState(){...}
      function subscribe(listener) {...}
      function dispatch(action) {...}
      function replaceReducer(nextReducer) {...}
      // 执行一次dispatch方法,目的为了每个reducer能拿到初始的state。
      dispatch({ type: ActionTypes.INIT })
      return {
        dispatch,
        subscribe,
        getState,
        replaceReducer
      }
    }

我们先分别介绍各个方法的实现

2.1 getState()

通过这个方法,我们可以取得最新的state

用法如下:

    const newState = store.getState()

源码中getState() 方法非常简单,它会直接返回内部变量currentState,这个变量的值就是state

   /**
   * 读取state
   */
  function getState() {
    if (isDispatching) { // 如果此时正在执行dispatch方法,会抛出错误。
      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
  }

源码中还做了一个判断,如果当前正在dispatch过程中,会抛出一个错误。

2.2 dispatch()

这个方法是我们唯一修改state的方法。它可以传入一个action,其中的type属性用来描述当前的行为。

用法如下:

    const addAction = {type: "add"}
    store.dispatch(addAction)

函数内部主要功能

  • 调用reducer()函数返回新的state
  • 调用所有订阅的函数,依次执行。

源码如下:

  function dispatch(action) {
    // 如果不是对象,抛出错误
    if (!isPlainObject(action)) {
      throw new Error(
        'Actions must be plain objects. ' +
          'Use custom middleware for async actions.'
      )
    }
    // 如果type为undefined抛出错误
    if (typeof action.type === 'undefined') {
      throw new Error(
        'Actions may not have an undefined "type" property. ' +
          'Have you misspelled a constant?'
      )
    }
    // 为了避免在reducer函数中调用dispatch(),这会导致无限循环。因此抛出错误。
    if (isDispatching) {
      throw new Error('Reducers may not dispatch actions.')
    }
    /**======================================================
        下面才是主要内容
    =======================================================**/ 
    try {
      isDispatching = true
      // currentReducer是createStore传入的reducer
      // 调用用户传入reducer函数,然后将值赋值给内部变量保存。
      currentState = currentReducer(currentState, action)
    } finally {
      isDispatching = false
    }
    // 取出保存的所有订阅的函数,然后执行。
    const listeners = (currentListeners = nextListeners)
    for (let i = 0; i < listeners.length; i++) {
      const listener = listeners[i]
      listener()
    }
    // 将传入action返回
    return action
  }

思考1:上面代码为什么使用try...finally,为什么没有使用try...catch

原因在于,用户传入reducer函数执行有可能会报错,由于没有使用catch,所以这个错误会被抛出,此函数终止运行。这个不难理解。

finally作用呢?它就是不管有没有报错,都会重置isDispatching,如果有报错,它可以防止接下来所有的dispatch()失效。

思考2:如果同时多次调用dispatch()方法,那么所有订阅的函数也会多次调用?或者dispatch()为啥不接收一个数组,从而可以派发多个aciton

如果同时多次调用dispatch()方法,所有订阅的函数也会多次调用,这是肯定得。

dispatch()为啥不接收一个数组作为参数。如果有出现这种问题,那么首先要考虑,你定义的action是否合适?如果完全可以用一个action来解决问题,那么就没有必要使用多个action。但是如果你非要想同时触发多个action,但是只触发一次订阅函数,那么可以使用高阶reducer的方式解决,详见

image.png

社区也有相关的插件

思考3dispatch()中为什么没有做判断:如果state改变才会调用所有订阅的函数,这不会提升性能吗?

是的,这会提升性能,但是redux把控制权交给了使用者,由使用者自行定义。如果你需要这样一种场景:每次dispatch()以后,不管state有没有变化,这样都会通知订阅者。那么你要感谢作者将控制权留给了你。

通过下面代码也可以知道: reducer()中不能使用getState(), dispatch(), subscribe()等方法。如果使用则会报错。

    try {
      isDispatching = true
      // 调用用户传入reducer函数,然后将值赋值给内部变量保存。
      currentState = currentReducer(currentState, action)
    } finally {
      isDispatching = false
    }

2.3 subscribe()

它可以订阅state的变化,它接收一个回调函数,这个函数会被添加到内部的队列中保存,state变化以后,回调函数就会被执行。它执行结果,返回一个函数,执行这个函数以后,之前传入的回调函数会从内部的队列中删除,达到取消订阅的目的。

用法如下:

    // 订阅
    const unsubscribe = store.subscribe(function stateChange() {
        console.log("newState:", store.getState())
    })
    // 取消订阅
    unsubscribe()

函数内部主要功能

  • 它会将订阅的回调函数添加到队列中保存。
  • subscribe()的返回值是一个方法,调用以后可以取消从队列中将回调函数删除。

源码如下:

  function subscribe(listener) {
    // 如果不是函数,抛出错误
    if (typeof listener !== 'function') {
      throw new Error('Expected the listener to be a function.')
    }
    // 如果此时正在执行dispatch方法,会抛出一个错误
    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 https://redux.js.org/api-reference/store#subscribelistener for more details.'
      )
    }
    /**======================================================
        下面才是主要内容
    =======================================================**/ 
    // 是否订阅的标志
    let isSubscribed = true
    // 主要作用: 如果nextListeners等于currentListeners,则currentListeners复制的结果赋值给nextListeners
    ensureCanMutateNextListeners()
    // 将订阅函数添加到队列
    nextListeners.push(listener)
    // 取消订阅函数
    return function unsubscribe() {
      // 如果已经从队列里面删除了,则直接返回。防止用户进行多次取消订阅。
      if (!isSubscribed) {
        return
      }
      // 正在`dispatch`过程中,会抛出一个错误
      if (isDispatching) {
        throw new Error(
          'You may not unsubscribe from a store listener while the reducer is executing. ' +
            'See https://redux.js.org/api-reference/store#subscribelistener for more details.'
        )
      }
      // 将取消订阅标志设置为false
      isSubscribed = false
      // 主要作用: 如果nextListeners等于currentListeners,则currentListeners复制的结果赋值给nextListeners
      ensureCanMutateNextListeners()
      // 移除掉订阅函数
      const index = nextListeners.indexOf(listener)
      nextListeners.splice(index, 1)
      currentListeners = null 
    }
  }

上面中的ensureCanMutateNextListeners()如下:

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

思考1:为什么需要nextListeners,它有什么作用吗?只有currentListeners不行吗?

我们知道nextListenerscurrentListeners都是用来保存订阅函数的,从函数ensureCanMutateNextListeners()中也可以,nextListeners的用处就是复制currentListeners。如果不复制行不行,或者只使用currentListeners会有什么问题吗?

其实:nextListeners的作用就是为了避免:在订阅器的回调函数执行时,用户继续订阅或者取消订阅,会引起bug,举个例子:

    // 如果增加了回调函数A
    const unsubscribe = store.subscribe(function A() {
      // 将自己取消订阅
        unsubscribe() 
    })

还记得dispatch() 函数中下面代码:

    // 取出保存的所有订阅的函数,然后执行。
    const listeners = (currentListeners = nextListeners)
    for (let i = 0; i < listeners.length; i++) {
      const listener = listeners[i]
      listener()
    }

在依次执行订阅器的回调函数时,比如A在队列中是第一个(即索引0),队列中还有B,C,D等回调。

如果A执行完以后,取消订阅,即从队列中将自己删除,那么此时listeners.length已经改变,B的索引为0。在下一轮循环,此时索引为1,就会导致B函数没有执行。 如果A执行完以后,又继续订阅,listeners.length也会改变,此时队列最后一个为新增的回调函数,那么在本轮循环它会立即触发,但是这不符合redux设计理念,新增的订阅函数应该在下一次dispatch()中触发。

思考2:接着思考1,有人可能又会说,那我们为什么将dispatch()改为下面的形式:

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

确实,改成这个形式,是可以解决这个问题,我最初也这么想的,直到我找到redux的代码提交记录: 可以看到最初redux代码也是类似这么写的,至于currentListeners和listeners谁复制谁?都不重要。

image.png

提交信息上表明大概的是:如果我们这么写,那岂不是每次dispatch都会复制一份队列吗?这会浪费资源,我们应该在需要的时候复制,即在订阅或者取消订阅时进行复制。

因此通过上面的这行代码const listeners = (currentListeners = nextListeners)可以保证,每次dispatch()时,currentListenersnextListeners一定相同,则肯定会执行ensureCanMutateNextListeners()函数,来进行复制。

订阅或者取消订阅时复制还有另外一个好处,可以保证当前订阅或者取消订阅会在下一次dispatch()中生效。

最后,ensureCanMutateNextListeners()函数为什么还需要判断if (nextListeners === currentListeners),直接复制一份不行吗?这样也是为了避免同时多次添加订阅回调函数时,而产生的不必要的复制。

思考3:取消订阅函数为什么末尾要将currentListeners = null

这是因为我们此时取消订阅时,仅仅把nextListeners中的回调订阅函数进行删除,但是此时currentListeners中仍会保存被取消的回调函数,这会引起内存泄漏。此时的currentListeners又没有被其他地方用到,完全可以设置为null。

思考4subscribe()为啥不将state作为参数传入回调函数中,而每次都使用store.getState()获得?

// 为什么需要这么获得`state`
store.subscribe(() => console.log(store.getState()));
// 而不是直接将`state`作为参数传递
store.subscribe(state => console.log(state));

我也感觉这是很方便的,这个问题也是开发者给redux提的issue比较多的一个问题,我找了的issue,终于找到了作者的回答

简要叙述下作者的意思是:

如果要是传入state作为参数,那么是不是也应该传入previousState,这样可以很方便开发者进行对比,而处理不同的逻辑?但是这要确保当store被Redux DevTools检测时,能够正常工作。作者认为每个使用getState()方法的store扩展,也不得不特别注意这个参数,感觉就像你为了让低级api对消费者更友好而付出的代价。

store的api是可扩展的,这就要求,每个函数的功能应该尽可能的单一化,原子化,而不应该重复。如果一个扩展想要在state传递给消费者之前做一些事情,如果我们传入state,那么我们不得不在两个地方处理,这很容易出错。

思考5subscribe()如果多次订阅同一个函数,会发生什么?

举个例子,有个公共函数,依次在组件A, B分别订阅了,此时的nextListeners会包含2个相同的函数,subscribe()函数里面并没有做任何校验,所以此时如果在B组件中取消了订阅,实际上是取消了组件A中的订阅的回调函数。

redux官方给出的解释是:这种情况很少见,认为并没有实际的使用场景。因此实际开发中我们要避免这种写法。

:在subscribe(function() {store.dispatch()})会导致无限循环。

2.4 replaceReducer()

它可以用于取代当前的reducer()函数。这是一个高级的api,如果你想要的动态的加载reducer,可以使用它。

用法如下:

    store.replaceReducer(newReducer)

源码如下:

  function replaceReducer(nextReducer) {
    // 如果不是函数则抛出错误。
    if (typeof nextReducer !== 'function') {
      throw new Error('Expected the nextReducer to be a function.')
    }
    // 将reducer重新赋值
    currentReducer = nextReducer
    // 触发一个action,以便于store.getState()能得到最新的值。
    dispatch({ type: ActionTypes.REPLACE })
  }

3 combineReducers()

随着应用变的越来越复杂,你可能会将reducer()函数分割成多个reducer(),并且每个reducer()对应独立的state。一个reducer()并不能满足要求,此时combineReducers()应运而生。

  • 它接收一个对象,该对象每一个key对应的value都为一个reducer()函数。
  • 它的返回值是合并好的reducer()函数。通过这个返回值计算出的state同样是一个对象,key值与传入combineRedeucers()key相同,value则为相应的state

用法如下:

    import {reducer1, reducer2, reducer3} = "./reducer"
    const reducer = combineReducers({reducer1, reducer2, reducer3})
    const store = createStore(reducer)

函数内部主要功能

  • 校验传入的参数是否为对象,每个key对应的reducer是否为函数,以及reducer是否设置默认值等。
  • 返回整合好以后的reducer。

该函数源码主要内容其实没有多少,大部分都是判断用户传入的值是否正确。

  1. 源码如下:
    function combineReducers(reducers) {
      // 获取传入的key
      const reducerKeys = Object.keys(reducers)
      // 经过处理过的reducers
      const finalReducers = {}
      // 循环,分别判断传入的reducer类型为undefined,则警告,如果类型为function则加入到finalReducers中。
      for (let i = 0; i < reducerKeys.length; i++) {
        const key = reducerKeys[i]
        if (process.env.NODE_ENV !== 'production') {
          if (typeof reducers[key] === 'undefined') {
            warning(`No reducer provided for key "${key}"`)
          }
        }

        if (typeof reducers[key] === 'function') {
          finalReducers[key] = reducers[key]
        }
      }
      const finalReducerKeys = Object.keys(finalReducers)

      // 缓存初始化传入的state中有,但finalReducers里没有的key。
      let unexpectedKeyCache
      if (process.env.NODE_ENV !== 'production') {
        unexpectedKeyCache = {}
      }

      let shapeAssertionError
      try {
        // 用于判断每个reducer是否设置默认值,以及reducer中是否使用了内置的actionType。
        assertReducerShape(finalReducers)
      } catch (e) { // 如果没有设置默认值或者使用redux内部的`actionType`则先缓存起来。
        shapeAssertionError = e
      }
      /**======================================================
        下面才是主要内容
      =======================================================**/ 
      // 最终的reducer函数
      return function combination(state = {}, action) {
        // 如果每个reducer没有设置默认值,或者使用了内置的actionType则为reducer函数,则抛出错误。
        if (shapeAssertionError) {
          throw shapeAssertionError
        }
        // 如果初始化传入的state中有,但finalReducers里没有的key。则抛出警告。
        if (process.env.NODE_ENV !== 'production') {
          const warningMessage = getUnexpectedStateShapeWarningMessage(
            state,
            finalReducers,
            action,
            unexpectedKeyCache
          )
          if (warningMessage) {
            warning(warningMessage)
          }
        }
        // 判断state是否改变
        let hasChanged = false
        // 最终的state
        const nextState = {}
        for (let i = 0; i < finalReducerKeys.length; i++) {
          const key = finalReducerKeys[i] // 当前key
          const reducer = finalReducers[key] // 当前reducer
          const previousStateForKey = state[key] // 旧的state
          const nextStateForKey = reducer(previousStateForKey, action) // 新的state
          // 如果新的state为undefined, 抛出错误。
          // 很多人可能好奇,之前不是已经校验reducer的返回值了,现在为啥还要校验。
          // 因为之前校验的使用内部的actionType,但是有可能用户针对某一个actionType返回undefined。主要是为了避免出现bug。
          if (typeof nextStateForKey === 'undefined') { 
            const errorMessage = getUndefinedStateErrorMessage(key, action)
            throw new Error(errorMessage)
          }
          nextState[key] = nextStateForKey
          // 开始hasChanged的值为false, nextStateForKey和previousStateForKey一旦不相等,则hasChanged 就为true,在下一轮循环就不需要继续对比它俩的值。
          hasChanged = hasChanged || nextStateForKey !== previousStateForKey
        }
        hasChanged =
          hasChanged || finalReducerKeys.length !== Object.keys(state).length
        return hasChanged ? nextState : state
      }
    }

由上述代码可知,判断hasChanged和为true的条件是

  • finalReducers中任意一个reducers返回值和上次不同,则为true。
  • finalReducerKeys的长度和初始化传入的state中的key的长度不同,则为true。

思考1 shapeAssertionError 为什么不直接抛出,而是在返回的函数里面抛出。

如果直接抛出的话,会导致整个js线程执行中止,creatStore()也会失败,随之react应用也会构建失败。作者认为在这里抛出错误,并不合适。而是在每次调用dispatch()时,就直接抛出一次,因此combination()里报错是最友好的。

思考2: 为什么判断finalReducerKeys.length !== Object.keys(state).length这个?

虽然之前有过检验初始化的key(即缓存在unexpectedKeyCache变量中),但只是抛出警告,并没有抛出错误,如果有unexpectedKeyCache(也就是上面等式不成立),则也会认为hasChangedtrue。(这个等式不成立的时刻也只有在第一次dispatch()时,因为后续的state都是根据finalReducers计算出来的,所以二者不可能不等。)

  1. 下面是assertReducerShape()函数的代码:
    function assertReducerShape(reducers) {
      // 遍历finalReducers,检测每个reducer结果是否为undefined,如果为undefined则报错
      Object.keys(reducers).forEach(key => {
        const reducer = reducers[key]
        // 使用内部的type:ActionTypes.INIT测试reducer的返回值
        const initialState = reducer(undefined, { type: ActionTypes.INIT })
        // 如果结果为undefined,说明该reducer没有返回默认值。
        if (typeof initialState === 'undefined') {
          throw new Error(
            `Reducer "${key}" returned undefined during initialization. ` +
              `If the state passed to the reducer is undefined, you must ` +
              `explicitly return the initial state. The initial state may ` +
              `not be undefined. If you don't want to set a value for this reducer, ` +
              `you can use null instead of undefined.`
          )
        }
        // ActionTypes.PROBE_UNKNOWN_ACTION()会返回一个随机的type
        // 进入到这一步,有两种情况:
        // 1,当前reducer里面对type为ActionTypes.INIT做了判断,并返回非undefined的值。
        // 2, 当前reducer里面没有对type为ActionTypes.INIT做判断,即:当reducer不识别某个actionType时,也会返回非undefined的值(这正是我们期望的)
        // 如果下面的:传入随机的type,reducer返回undefined,可以排除上面第2种情况,说明代码中使用仅供redux内部使用的type,此时抛出错误。
        if (
          typeof reducer(undefined, {
            type: ActionTypes.PROBE_UNKNOWN_ACTION()
          }) === 'undefined'
        ) {
          throw new Error(
            `Reducer "${key}" returned undefined when probed with a random type. ` +
              `Don't try to handle ${ActionTypes.INIT} or other actions in "redux/*" ` +
              `namespace. They are considered private. Instead, you must return the ` +
              `current state for any unknown actions, unless it is undefined, ` +
              `in which case you must return the initial state, regardless of the ` +
              `action type. The initial state may not be undefined, but can be null.`
          )
        }
      })
    }

思考1:假如,我们在createStore()传入了初始值,但是单独的reducer()函数没有设置初始值,会发生什么?

举个例子:

    import {reducer1, reducer2, reducer3} = "./reducer"
    const reducer = combineReducers({reducer1, reducer2, reducer3})
    const initState = {reducer1: 1, reducer2: 2, reducer3: 3}
    // 传入初始值
    const store = createStore(reducer, initState)

reducer1函数类似下面的形式,即函数中没有设置默认值:

    reducer1(state, action) {
       return state 
    }

在上面的assertReducerShape()函数中有一行代码:const initialState = reducer(undefined, { type: ActionTypes.INIT }),这个代码中由于传入undefined,并没有传入createStore()初始值,所以会报错。

因此,设置初始值,我们最好是在每个独立的reducer()函数中。

思考2:接着思考1,assertReducerShape()的每个独立的reducer()为什么通过传入undefined来进行判断?

因为传入undefined,因为reducer()函数返回undefined是没有任何意义的,通过传入undefined能够更好的判断用户写的reducer()函数的健壮性。

思考3:每次dispatch()以后,所有的reducer()都会执行,这会影响性能吗?

官方认为,虽然会影响,但可以忽略不计,因为js引擎每秒可以运行大量的函数,并且reducer函数还是纯函数,全有能力hold住。如果你确实担心这方面的性能问题,可以使用插件。

4 bindActionCreators()

bindActionCreators()是将actionCreator()dispatch()函数进行绑定。 它接收2个参数,第一个参数为函数/对象,第二个参数为dispatch

  • 如果第一个参数为函数(某个actionCreator()),则返回一个函数(actionCreator()dispatch()整合函数)

  • 如果第一个参数为对象(多个actionCreator()),则返回一个包含相同key的对象。其一般用于react-redux中的mapDispatchToProps上面。

用法如下:

    const actionCreators = {
        addAction(value) {
            return {type: "ADD", value}
        },
        delAction(value) { 
            return { type: 'DEL', value }
        }
    }
    const boundActions = bindActionCreators(action, dispatch)
    boundActions.addAction(2); // 相当于dispatch(actionCreators.addAction(2))
    boundActions.delAction(1); // 相当于dispatch(actionCreators.delAction(1))

函数内部主要功能

  • dispatch()方法和actionCreator()进行绑定。

actionCreator(),我们已经知道action是一个对象,形如{type: "ADD", value: 2}actionCreator()顾名思义就是生成action的函数,如下:

    // getAddAction就是一个actionCreator()函数
    const getAddAction = (value) => {type: "ADD", value}

源码如下:

    function bindActionCreators(actionCreators, dispatch) {
      // 如果为函数,则直接调用bindActionCreator函数
      if (typeof actionCreators === 'function') {
        return bindActionCreator(actionCreators, dispatch)
      }
      // 如果actionCreators不是对象,并且不为null,则直接抛出错误
      if (typeof actionCreators !== 'object' || actionCreators === null) {
        throw new Error(
          `bindActionCreators expected an object or a function, instead received ${
            actionCreators === null ? 'null' : typeof actionCreators
          }. ` +
            `Did you write "import ActionCreators from" instead of "import * as ActionCreators from"?`
        )
      }
      // 是对象,则循环,依次调用bindActionCreator函数
      const boundActionCreators = {}
      for (const key in actionCreators) {
        const actionCreator = actionCreators[key]
        if (typeof actionCreator === 'function') {
          boundActionCreators[key] = bindActionCreator(actionCreator, dispatch)
        }
      }
      return boundActionCreators
    }

bindActionCreator()源码如下:

    function bindActionCreator(actionCreator, dispatch) {
      return function() {
        return dispatch(actionCreator.apply(this, arguments))
      }
    }

5 compose()

compose函数的主要功能就是:将多个函数,如a,b,c作为参数传入(compose(a, b, c)) 并转为(...args) => a(b(c(args)))形式。一般用于组合多个中间件函数。

源码如下:

    export default function compose(...funcs) {
      // 没有参数,则返回一个默认的函数arg => arg
      if (funcs.length === 0) {
        return arg => arg
      }
      // 如果只有一个参数,那就将其返回
      if (funcs.length === 1) {
        return funcs[0]
      }
      // 转化
      return funcs.reduce((a, b) => (...args) => a(b(...args)))
    }

6 applyMiddleware()

applyMiddleware()接收多个中间件函数,用法如下:

    const store = createStore(reducer, applyMiddleware(middlewareFn))

6.1 中间件函数的由来

官网例子

  1. 如果在每次一状态改变以后,都会将action和新的state记录,通常我们会像下面这么做:
    const action = addTodo('Use Redux')

    console.log('dispatching', action)
    store.dispatch(action)
    console.log('next state', store.getState())
  1. 上面的方式可以实现,但是我们不想每次都写那么多代码,这是可能会做如下封装:
    function dispatchAndLog(store, action) {
      console.log('dispatching', action)
      store.dispatch(action)
      console.log('next state', store.getState())
    }
    
    // 调用
    dispatchAndLog(store, addTodo('Use Redux'))
  1. 此时,虽然会解决问题,但是每次都会引入函数(dispatchAndLog())并调用dispatchAndLog(store, addTodo('Use Redux')),这时我们可能会继续做如下封装:
    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
    }

通过将dispatch重新赋值,我们可以一劳永逸。但是问题又出现了,假如我要应用多次这样的变换呢?比如:我每次dispatch(),我还需要记录错误日志呢,这会应该怎么办?

  1. 如果我们要实现一个上报错误的中间件呢?(有人可能会说,记录错误,可以使用window.onerror,但是它并不是可靠的,因为在一些较旧的浏览器中,它不提供堆栈信息(这对于理解为什么会发生错误至关重要)。)如果调用dispatch()方法以后,发生任何报错,都会上报,这岂不很好吗?

我们知道,保持记录和上报错误这二者的分离,也是很重要的,他们应该属于不同的模块,因此我们可能会做如下封装:

    function patchStoreToAddLogging(store) {
      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
      }
    }

    function patchStoreToAddCrashReporting(store) {
      const next = store.dispatch
      store.dispatch = function dispatchAndReportErrors(action) {
        try {
          return next(action)
        } catch (err) {
          console.error('Caught an exception!', err)
          // 进行错误上报,例如:
          postData("/error", {error, action, state: store.getState()})
          throw err
        }
      }
    }
    // 调用
    patchStoreToAddLogging(store)
    patchStoreToAddCrashReporting(store)
  1. 但是上面的仍然不是很好,侵入性比较强,属于MONKEY PATCHING(即:猴子补丁,说的是在函数或对象已经定义之后,再去改变它们的行为。))希望的是每一个功能函数,尽可能不会修改外面的变量或者函数,从而产生一些副作用。如果我们将这个函数返回,交由外面去处理。例如:如果我们将上面的函数变成如下这种形式,岂不很好:
    function logger(store) {
      const next = store.dispatch
      // 之前:
      // store.dispatch = function dispatchAndLog(action) {
      
      return function dispatchAndLog(action) {
        console.log('dispatching', action)
        let result = next(action)
        console.log('next state', store.getState())
        return result
      }
    }

并且同时在redux内部也可以一个“助手”,来帮助我们处理这些:

    function applyMiddlewareByMonkeypatching(store, middlewares) {
      // 这一步的目的是, 在之前的代码,我们应用了两个中间件,随后当我们dispatch一个action时, 最后的中间件先执行,这可能明显不符合大部分人的习惯。所以需要转换
      middlewares.reverse()
      
      middlewares.forEach(middleware => (store.dispatch = middleware(store)))
    }
    
    // 此时我们就可以使用:
    applyMiddlewareByMonkeypatching(store, [logger, crashReporter])
  1. 但是上面的logger函数,仍然属于MONKEY PATCHING(即:猴子补丁,说的是在函数或对象已经定义之后,再去改变它们的行为。)。 其实logger还有另外一种方式实现,如果我们不用next = store.dispatch而是作为参数实现呢?
    function logger(store) {
      return function wrapDispatchToAddLogging(next) {
        return function dispatchAndLog(action) {
          console.log('dispatching', action)
          let result = next(action)
          console.log('next state', store.getState())
          return result
        }
      }
    }

那么此时,我们将applyMiddlewareByMonkeypatching改名为applyMiddleware,并将它变为下面这个:

    function applyMiddleware(store, middlewares) {
      middlewares.reverse()
      let dispatch = store.dispatch
      middlewares.forEach(middleware => (dispatch = middleware(store)(dispatch)))
      return Object.assign({}, store, { dispatch })
    }

最终这个applyMiddleware函数和源码中的很像了,但是还有三方面不同:

  • 源码中并没有暴露store整个api,而是将dispatch()getState()方法。
  • 源码中做了一个判断,如果在构建中间件的时候,调用dispatch()方法则会报错。
  • 为了确保中间件只能应用一次,所以应用中间件的时机是在createStore()执行时。

源码如下:

    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(reducer, preloadedState, enhancer)函数里的enhancer(createStore)(reducer, preloadedState)这行代码吗?而这个enhancer参数通常就是applyMiddleware()

思考1: 为什么源码中这么写dispatch: (...args) => dispatch(...args)而不是dispatch: dispatch

从源码中看到中间件的函数主要作用就是返回一个新的dispatch,如果直接dispatch: dispatch这么写,那么此时的middlewareAPI.dispatch就永远是下面这个函数:

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

栗子:

    let fn = () => console.log("a")
    let obj = {fn}
    fn = () => console.log("b")
    console.log(obj.fn) // 还是为() => console.log("a")

思考2:那么问题又来了,接着思考1,为什么要给dispatch的初始值设置为抛出错误的函数?

就是为了防止在中间件构建时(即中间件内部第一层,第二层)立即调用dispatch()方法。

例如下面一个中间件:

    ({ dispatch, getState }) => {
        如果此时立即调用dispatch()则会报错。
        dispatch()
        return next => action
    }

思考3:我们自己的applyMiddleware赋值了三次dispatchmiddlewares.forEach(middleware => (dispatch = middleware(store)(dispatch))) 而,实际的源码却只赋值了一次,这是怎么做到的?

就是通过compose()方法实现的,它可以将形如(compose(a, b, c)) 并转为(...args) => a(b(c(args)))形式。这个返回结果再传入dispatch就可以将dispatch()一层一层包裹起来。

如:(...args) => a(b(c(args)))此时将dispatch传入以后。

  • c(args)中的args(即此时的next())就是原始的dispatch(),返回值就是c中间件返回的新dispatch()
  • 同理:对于函数b的参数(此时的next())就是c的dispatch()
  • 对于函数a的参数(此时的next())就是b的dispatch()

因此最终当我们调用store.dispatch()方法时,最终各个中间件的执行顺序就是:

  • a的dispatch()开始执行。
  • b的dispatch()开始执行。
  • c的dipatch()开始执行。
  • c的dispatch()执行结束。
  • b的dispatch()执行结束。
  • a的dispatch()执行结束。

执行顺序就是洋葱模型:

middleware.drawio.png

因此在中间件第三层调用dispatch以后会将所有的中间件都执行一遍。而调用next则会调用下一个中间件。

7 redux-thunk源码

thunk中间件函数就不难理解了,只不过多了传入参数的功能extraArgument。

    function createThunkMiddleware(extraArgument) {
      return ({ dispatch, getState }) => (next) => (action) => {
        // 如果要是个函数就将dispatch传入。
        if (typeof action === 'function') {
          return action(dispatch, getState, extraArgument);
        }

        return next(action);
      };
    }

    const thunk = createThunkMiddleware();
    thunk.withExtraArgument = createThunkMiddleware;

    export default thunk;

使用方法

    const store = createStore(reducer, applyMiddleware(thunk))
    
    const asyncAction = async (dispatch) => {
        let res = await postData('/a')
        dispatch(action)
    }
    // 此时可以派发一个函数
    store.dispatch(asyncAction)