Redux——一个源码比使用简单的库

相信学过 React 的朋友对 Redux 一定不陌生,其作为 JavaScript 的状态容器,能够提供可预测化的状态管理,但其实它并不是专门为了 react 而设计的,你也可以在原生 JS 项目或者Vue 项目中使用它。不过因为其设计理念与 react 相似,和 react 一起使用时会有较好的开发体验,以至于很多人一提到 redux 就会联想到 react

那么至于它好不好用呢?这个问题我相信从它的流行程度上大家就能看的出来——非常好用!但是它的上手难度较高,以至于我一开始接触 redux 的时候觉得它很玄学,学的时候迷迷糊糊的,只敢跟着老师敲代码,自己去做项目的时候也总需要 copy 之前项目中 redux相关的代码。导致 redux 在我心中好像是神圣而不可侵犯的,但无意间听到别人说 redux 的源码就几百行,我惊呆了,那么牛*的库竟然只有几百行代码,决定一定要花时间把它拿下💪

核心源码解析

话不多说,我们直接开始,不过在该文章中展示的代码并不是源码中所有的代码,有一些函数是供 redux 开发者来使用的,比如 replaceReducerobservable,作为一名合格的大学生,在自身没有达到很高的水平前,这种东西我肯定是能不看就不看哈哈~

另外,很多文件中都会有大量注释和警告信息,为了让代码看起来更简洁,这篇文章也会根据实际情况做一个删减,如果想要补充的可以去 github 上下载 redux 的源码进行研究。即使如此,本文的代码量依然比较大,因为为了让大家更全面的理解 redux,但凡是有点作用的代码我都没有删掉,希望大家可以耐心看完~😊

utils

我们先去看一些比较重要的工具函数,他们存放于源码中的 utils 文件夹内。一开始就看工具函数当然是有原因的,因为这些函数会在下文中关键的地方被调用,如果没有理解其内部操作的原理,可能会影响你对整个 redux 源码的理解

actionTypes.js

用过 redux 的朋友肯定都知道,进行派发操作 dispatch 时需要一个 action 变量,该变量一定有个名为 type 的属性,它决定了 reducer 函数要返回的新状态,而下面返回的 ActionTypes 变量就是 redux 内部的 action 变量,至于这有啥作用,下文中会详细讲到

// 生成一个随机字符串,导致下面 action 的 type 类型每次都会不一样
const randomString = () =>
Math.random().toString(36).substring(7).split('').join('.')

// redux 内置的 action,其具有 3 个type,但有一个没有那么重要,所以这里就省去了
const ActionTypes = {
  // 用于测试 reducer 函数内部有没有对未定义的 type 进行判断
  INIT: `@@redux/INIT${randomString()}`,
  // 用于测试 reducer 函数内部有没有对未知的 type 进行判断
  PROBE_UNKNOWN_ACTION: () => `@@redux/PROBE_UNKNOWN_ACTION${randomString()}`,
}

export default ActionTypes
isPlainObject.js

这个函数用于判断用户传入的参数是不是一个真正的对象,因为仅用 typeof 并不能精准的判断一个变量的类型,所以这里还额外使用了原型的方法加以判别。其实下面的方法也可以在我们自己的项目中使用,毕竟学习源码最重要的是去学习它的思想

// 检查传入的参数是不是一个真正的对象
export default function isPlainObject(obj) {
  // 先使用 typeof 筛选掉一些类型
  if (typeof obj !== 'object' || obj === null) return false
  
  let proto = obj
  // 补充:Object.getPrototypeOf 方法用于获取一个对象的原型,也就是其 __proto__ 属性
  // 正常情况下,对象类型原型链的顶层其实就是 Object.prototype
  while (Object.getPrototypeOf(proto) !== null) {
    proto = Object.getPrototypeOf(proto)
  }
  
  // 真正的对象都是由 Object 函数构造出来的,所以对象的原型即其 __proto__ 属性就等于 Object.prototype
  // 但是这里排除了 obj 是通过 Object.create(null) 创建出来的情况以及 obj = Object.prototype 的情况
  return Object.getPrototypeOf(obj) === proto
}
kindOf.js

这个函数封装的也很不错,同样是可以在我们自己的项目中使用的,其用来判断一个变量的具体类型

function miniKindOf(val) {
  // 返回能用 typeof 就判断出的类型
  if (val === void 0) return 'undefined'
  if (val === null) return 'null'
  
  const type = typeof val
  switch (type) {
    case 'boolean':
    case 'string':
    case 'number':
    case 'symbol':
    case 'function': {
      return type
    }
    default:
      break
  }
  
  // 对象类型需要详细判断
  // 判断变量是不是数组类型
  if (Array.isArray(val)) return 'array'
  // 判断变量是不是Date类型
  if (isDate(val)) return 'date'
  // 判断变量是不是Error类型
  if (isError(val)) return 'error'
  
  // 返回变量构造器(一般为其构造函数)的名称
  const constructorName = ctorName(val)
  switch (constructorName) {
    case 'Symbol':
    case 'Promise':
    case 'WeakMap':
    case 'WeakSet':
    case 'Map':
    case 'Set':
      return constructorName
    default:
      break
  }

  // 其它类型
  return type.slice(8, -1).toLowerCase().replace(/\s/g, '')
}
 
// 获取变量构造器(一般为其构造函数)的名称
function ctorName(val) {
  return typeof val.constructor === 'function' ? val.constructor.name : null
}

// 判断变量是不是 Error 对象
function isError(val) {
  return (
    val instanceof Error ||
    (typeof val.message === 'string' &&
      val.constructor &&
      typeof val.constructor.stackTraceLimit === 'number')
  )
}

// 判断一个变量是不是 Date 对象
function isDate(val) {
  if (val instanceof Date) return true
  return (
    typeof val.toDateString === 'function' &&
    typeof val.getDate === 'function' &&
    typeof val.setDate === 'function'
  )
}

// 返回参数的类型
export function kindOf(val) {
  // 如果是在生产环境中,则直接用 typeof 判断类型,否则用更复杂的 miniKindOf 函数来判断类型
  let typeOfVal = typeof val

  if (process.env.NODE_ENV !== 'production') {
    typeOfVal = miniKindOf(val)
  }

  return typeOfVal
}
warning.js

顾名思义,redux 封装了一个用于打印警告信息的函数,注意:该函数只是将错误信息打印了出来而已,并没有向外界抛出错误,所以即使执行了这个函数也不会阻塞代码的执行

// 将警告信息打印在控制台中
export default function warning(message) {
  if (typeof console !== 'undefined' && typeof console.error === 'function') {
    console.error(message)
  }
}

index.js

作为整个项目的入口文件,当我们在自己的项目中导入 redux 时,实际上导入的是 index.js 文件的导出值,例如 createStorecombineReducersapplyMiddlewarecompose 等函数

import { createStore } from './createStore'
import combineReducers from './combineReducers'
import applyMiddleware from './applyMiddleware'
import compose from './compose'
import warning from './utils/warning'

// 这里定义了一个“假函数”,它的目的是为了去检查当前代码有没有被打包工具压缩过(比如 webpack)
function isCrushed() { }

// 通过函数名是否更改来判断代码是否被压缩过,如果代码被压缩过,则打印警告
if (
  process.env.NODE_ENV !== 'production' &&
  typeof isCrushed.name === 'string' &&
  isCrushed.name !== 'isCrushed'
) {
  warning('警告...')
}

export {
  createStore, 
  combineReducers, 
  applyMiddleware,
  compose
}

createStore.js

这是整个 redux 中最重要的一个文件,该函数用于创建了一个 store 对象,里面包含三个核心功能: 唯一获取当前状态的 getStore 方法、唯一更新状态的 dispatch 方法和监听状态变换的 subscribe方法。使用过 redux 的朋友一定对这几个函数名称不陌生,下面我们先来将这个函数的逻辑走通,里面具体的方法后面再单独介绍

export function createStore(reducer, preloadedState, enhancer) {
  // 前面一大段代码其实都是进行参数的类型检查
  /**
   * && 的优先级是比 || 要高的,有两种情况可以命中判断逻辑: true || any 或者 false ||true
   * 如果传递进来的 preloadedState 和 enhancer 参数都是函数类型的话,那么就认为你传递了多个 enhancer 函数
   * 如果传递进来的 enhancer 和 第四个参数都是函数的话,redux 也会认为你传递了多个 enhancer 函数
   * 但 redux 只能接收一个 enahncer 函数,所以你所传的多个 enhancer 参数需要提前用 compose 去合并成一个函数的
   * 如果 redux 认为你传入了多个 enhancer 参数则会报错
   */
  if (
    (typeof preloadedState === 'function' && typeof enhancer === 'function') ||
    (typeof enhancer === 'function' && typeof arguments[3] === 'function')
  ) {
    throw new Error('警告...')
  }

  /**
   * 如果传递了 preloadedState 参数但没有传递 enhancer 参数,而且 preloadedState 类型为函数的话
   * 那么就会将 preloadedState 当成是 enhancer 来使用,下面做的是交换变量值的操作
   */
  if (typeof preloadedState === 'function' && typeof enhancer === 'undefined') {
    enhancer = preloadedState
    preloadedState = undefined
  }

  // 如果有传递 enhancer 参数但类型不为函数的话,redux 会报错,因为 redux 要求 enhancer 必须是个函数
  if (typeof enhancer !== 'undefined') {
    if (typeof enhancer !== 'function') {
      throw new Error(
        // 这里就用到了 kindOf 函数来精确用户传入的错误参数的类型
        `Expected the enhancer to be a function. Instead, received: '${kindOf(enhancer)}'`
      )
    }

    /**
     * 当有 enhancer 函数时,createStore 函数的返回值是 enhancer(createStore)(reducer, preloadedState)
     * 通过这个函数调用的顺序和传递的参数也说明了 enhancer 函数必须要满足下列要求:
     * 其必须要有一个参数接收传入的 createStore 函数
     * 而且该函数的返回值又是一个函数,并且需要设置两个参数分别接收传递给 createStore 的 reducer 和 preloadedState 参数
     * 那么 enhancer 函数的形状大概可以确定了,其相当于接收原始的 createStore 函数然后返回了一个加强版的 createStore 函数
     * 并利用传递给 createStore 的参数构建了一个加强版的 store
     * 至于这个新的 store 与用 createStore 函数直接构造出的有什么不同?我们后面会详细介绍
     */
    return enhancer(createStore)(reducer, preloadedState)
  }

  // 传入的 reducer 参数类型必须是一个函数,如果有多个 reducer 的话需要使用 combineReducers 合并成一个 reducer 后再传入,否则报错
  if (typeof reducer !== 'function') {
    throw new Error(
      `Expected the root reducer to be a function. Instead, received: '${kindOf(reducer)}'`
    )
  }

  // redux 当前的 reducer,不过这个 reducer 一般不会变更
  var currentReducer = reducer;
  // redux 当前的状态,初始状态即调用 createStore 时传入的初始状态
  var currentState = preloadedState;
  // 用于存储通过 store.subscribe 订阅的所有监听器
  var currentListeners = [];
  // 最新的 listeners 数组,其存在是为了避免一些特殊情况所产生的 bug,后面会有具体介绍
  var nextListeners = currentListeners;
  // 标志着当前是否处于 dispatch 状态(从开始 dispatch 到 reducer 函数执行完更新状态,这个变量都为 true,但监听器数组被遍历执行之前该变量变为 false)
  var isDispatching = false;

  // 将 currentListeners 浅拷贝给 nextListeners 是为了避免一些特殊情况下的 bug,比如用户在监听器中又订阅或取消订阅了其他监听器,这样会导致监听器数组在执行过程中发生变化
  // 该函数具体在哪里使用后续会详细介绍
  function ensureCanMutateNextListeners() {
    if (nextListeners === currentListeners) {
      // 下面代码相当于 nextListeners = [...currentListeners],本质上就是让 nextListeners 的元素值不变,但更换它的引用,避免 nextListeners 的更改影响到原来的监听器数组 currentListeners
      nextListeners = currentListeners.slice()
    }
  }

  // redux 中唯一获取 store 状态的方法
  function getState() { ... }

  // 订阅监听器的函数,订阅的监听器会在每次 dispatch 之后(状态更新之后)按照所有监听器订阅的顺序依次执行
  function subscribe(listener) { ... }

  // 通过 action 唯一更改 store 状态的函数
  function dispatch(action) { ... }

  // 返回 store 之前先执行 dispatch 函数将 store 的状态更新为开发者设置的初始值,这也可以解释为什么在我们的项目中明明没有进行 dispatch,但是 reducer 会先执行一次
  // 整个流程就是 disaptch 派发 action --> reducer 函数执行并返回新状态 --> 更新状态,这样执行 getState 方法获取到的就是初始的状态值了
  dispatch({ type: ActionTypes.INIT })

  return {
    dispatch,
    subscribe,
    getState
  }
}

看完该函数之后,相信大家对其实现的具体思路有了一定的了解,总结一下 createStore 有几个重要的点:

  • 在使用 createStore 创建 store 的时候会自动执行一次 dispatch 函数,目的就是为了将reducer 中设置的初始状态更新到 store
  • createStore 并不复杂,真的就只是创建了一个 store对象并在里面集合了一些核心的方法而已,真正重要的还是里面具体方法的实现

下面我们来具体看下每个核心的方法具体都是怎么实现的?实现了什么?

getState

作为 redux 中唯一获取 store 状态的方法,其内部实现非常简单,因为 store 的状态其实就是在 createStore 函数中定义的 currentState 变量,该函数做的事情也只不过是将该值返回出去了而已

// 获取存储在 store 中的状态
function getState() {
  // 判断当前是否正处于 dispatch 派发状态,如果处于则抛出错误,否则返回状态值
  if (isDispatching) {
    throw new Error('警告...')
  }

  return currentState
}

不过比起这个函数是怎么实现的?我觉得我们应该考虑一个更深层次的问题,这里 redux 为什么不直接将 currentState 暴露出去,比如直接放置在 store 对象上,这样开发者们就可以直接通过 store.currentState 获取到当前的状态值了,操作不是变得更加简单吗?

但其实这恰恰是一个第三方库基本的开发规则——不随意将内部要使用的变量直接暴露出去,而要让开发者通过一个函数去获取对应的值,这样这个值才能在你的可控范围之内。那为什么要这样做呢?

因为你不知道开发者会如何使用这个变量,比如可能会有人直接对该属性进行赋值,从而影响你在库中对该变量的使用,进而导致你开发的库失去原先的功能。而我们作为开发者也要警惕此类问题的发生,如果真的遇到了第三方库将内部属性直接暴露出来的情况,我们也要确保不对其进行修改,相当于把它看成一个只读属性来使用

dispatch

dispatch 可以说是 createStore 中最核心的一个函数,因为它是唯一一个能够改变 store 中状态的函数。其内部主要做的事情就是触发 reducer 函数,然后将 reducer 函数的返回值更新为 store的新状态,最后再去遍历监听器数组执行所有的监听器函数,该函数工作的流程大概就是 dispatch —> reducer —> 更新状态 —>subscribelisteners。但对于如何使用 dispatch 函数,redux 有两点硬性要求:

  • 传入 dispatchaction 一定要是纯粹的对象类型,如果你想传入其它的类型,那么必须要使用中间件进行处理,比如 redux-thunk
  • 如果 action 是对象类型,那么其必须要具有一个type 属性来表示本次更新状态的类型
function dispatch(action) {
  // 判断传入的函数是不是对象
  if (!isPlainObject(action)) {
    throw new Error('警告...')
  }

  // 派发的 action 必须要有 type 类型
  if (typeof action.type === 'undefined') {
    throw new Error('警告...')
  }

  // 正在 dispatch 的过程中不能再次 dispatch 
  if (isDispatching) {
    throw new Error('警告...')
  }

  try {
    // 参数类型判断没有问题之后会将 isDispatching 设置为 true,在变为 false 之前用户不能再次 dispatch、订阅和取消订阅
    isDispatching = true
    /**
     * currentReducer 就是我们调用 createStore 创建 store 的时候传入的 reducer函数
     * dispatch 函数内部会调用 reducer,并将当前 store 的状态和派发的 action 传递进去,最终 reducer 的返回值会被作为新状态更新到 currentState 上
     */
    currentState = currentReducer(currentState, action)
  } finally {
    // 状态更新完毕,将 isDispatch 的状态设为 false,现在就允许开发者订阅/取消订阅以及再次 dispatch 更新状态了
    isDispatching = false
  }

  // 因为每次订阅/取消订阅更改的都是 nextListeners 数组,所以 nextListeners 记录了最新的监听者情况,所以要先进行赋值操作后再遍历
  const listeners = (currentListeners = nextListeners)
  // 按照数组中的元素顺序依次遍历执行监听器数组的函数
  for (let i = 0; i < listeners.length; i++) {
    const listener = listeners[i]
    listener()
  }

  return action
}

在其他函数中,都会有对 isDispatching 变量的判断,看了其内部代码之后,我们发现该变量会在 dispatch 函数一执行(如果没有参数类型错误的话)的时候就变为 truereducer 函数执行完成并将 store 的状态更新之后该变量才会变为 false,这样也就保证了开发者不能在 reducer 函数里面订阅/取消订阅函数。但我们要注意在遍历监听器数组的时候,该变量的值已经变为了 false,所以开发者是可以在监听器里面订阅/取消订阅其它函数的,我们下面会详细讲到这个问题

subscribe

这个函数的作用就是订阅一个监听器,具体实现也很简单,直接将要监听的函数 pushnextListeners 数组里面,那么在下次 dispatch 函数执行的过程中,就会去遍历执行对应的监听器数组,新订阅的监听器也会按照订阅的次序依次被执行

function subscribe(listener) {
  // 订阅的 listener 必须要是函数类型,否则报错
  if (typeof listener !== 'function') {
    throw new Error(
      `Expected the listener to be a function. Instead, received: '${kindOf(listener)}'`
    )
  }

  // 如果正处于 dispatch 状态则不允许订阅函数,直接报错
  if (isDispatching) {
    throw new Error('警告...')
  }

  // 标志该监听器已经被订阅了
  let isSubscribed = true
  
  // 浅拷贝一份监听器数组,让 nextListeners 和 currentListeners 不是同一个引用,避免本次监听器数组 nextListeners 的更改影响到正在遍历的监听器数组 listeners
  ensureCanMutateNextListeners()
  // 将刚刚所加的监听器压入到 nextListeners 订阅数组中
  // 上面的拷贝就是为了本次对订阅者数组的修改不影响到 listeners函数,从而不影响本次监听器数组的遍历
  nextListeners.push(listener)

  // 返回一个取消订阅的函数,后续将具体讲解
  return function unsubscribe() { ... }
}

上述过程中调用了一个很关键的函数:ensureCanMutateNextListeners。我们之前在阅读 createStore 函数的时候就已经看过它的具体实现了,里面其实也就是对 currentListeners 做了一层浅拷贝并赋值给了 nextListeners 而已,并没有什么复杂的操作,那么这一步操作到底有什么目的呢?

其实就是为了防止一种特殊情况的发生:在监听器函数里面又订阅或取消订阅了监听器,我们先想一下,如果没有执行这个函数,那么在监听器函数里面再订阅另外的监听器会有发生什么情况?

Redux——一个源码比使用简单的库

订阅的时候是将新的监听器直接压入到了 nextListeners 数组中,而现在 nextListenerscurrentListeners 是同一个引用,指向的数组新增了一个监听器,所以在此次监听器数组的遍历中,新增的那个监听器也会被执行,但这可能并不是开发者想要的效果

然后我们再来看一下如果在订阅之前执行了 ensureCanMutateNextListeners 函数,这个过程会发生什么变化呢?

Redux——一个源码比使用简单的库

有了这个函数之后,就算开发者在监听器里面又订阅了其它的监听器,由于 currentListenersnextListeners 的合作,根本不会影响本次监听器数组的执行结果的。别看 reudx 代码量小,但细节还是有的👍

unsubscribe

既然有订阅监听器,那么肯定就要有取消监听器,类似于你在 react 中的 useEffect 钩子里面监听的事件都需要 return 一个函数在组件卸载时予以清除。redux 中清除订阅者的方式很简单,就是将对应的监听器从 nextListeners 中移除而已

function unsubscribe() {
  // 如果该订阅的函数已经被取消了,那么就不需要重复取消
  if (!isSubscribed) {
    return
  }

  // 如果正处于 dispatch 状态则不允许取消订阅函数
  if (isDispatching) {
    throw new Error('警告...')
  }

  // 将之前设置的标志改为 false,标明该监听器已经被取消了
  isSubscribed = false
  // 依然要拷贝一份监听器数组,避免本次监听器数组 nextListeners 的更改影响到正在遍历的监听器数组 listeners 
  ensureCanMutateNextListeners()
  // 直接找到对应监听器在监听器数组的索引并将其移除
  const index = nextListeners.(listener)
  nextListeners.splice(index, 1)
  // 清空 currentListeners
  currentListeners = null
}

看到这里,我们会发现无论是注册监听器还是取消监听器,我们操作的都是 nextListeners 数组而不是 currentListeners 数组,其实这都是怕开发者在监听器执行的过程中订阅/取消订阅了监听器,从而导致数组遍历前与遍历的过程中监听器数量发生变换。在监听器里面取消订阅的情况我这里就不画图了,和订阅十分类似,想要深入理解这一过程的朋友可以像我在订阅部分一样画一份流程图出来🤞

补充

redux 官网对自己的介绍就是: A Predictable State Container for JS Apps,意思就是 redux 所管理的状态是可以预测的,因为它每次状态的更新都是 reducer 函数的返回值,而这个函数要求是一个纯函数,不能有任何的副作用,只要传入的旧状态 statedispatch 派发的 action 是确定的,那么返回的新状态 state 也必须要是确定的,所以才说它的状态是可预测的,下面是一个 reducer示例:

function reducer(state = defaultState, action) {
  switch (action.type) {
    case actionTypes.CHANGE_TOP_BANNERS:
      return {...state, ...action.topBanners}
    case actionTypes.CHANGE_HOT_RECOMMEND:
      return {...state, ...action.hotRecommend}
    case actionTypes.CHANGE_NEW_ALBUMS:
      return {...state, ...action.newAlbums}
    default:
      return state
  }
}

combineReducer.js

上面看的源码中,我们会发现 createStore 只允许传递一个 reducer 函数,但是在实际的项目中,我们并不会将所有模块的状态处理逻辑都写在一起,比如消息模块、点赞模块和收藏模块等等,如果都写在一起的话,一个 reducer 函数的逻辑就会过于复杂,不利于后期的维护

因此我们希望可以按照模块来将 reducer 进行划分,每个模块对应一个 reducer,最后再通过某种方法将每个模块的 reducer 合并起来形成一个总的 reducer 传入到 createStore 中。combineReducer 函数做的就是这样的事情,下面是使用 combineReducers 合并子 reducer 的示例:

const reducers = combineReducers({
    recommend: recommendReducer,
    detail: detailReducer
})
import ActionTypes from './utils/actionTypes'
import warning from './utils/warning'
import isPlainObject from './utils/isPlainObject'
import { kindOf } from './utils/kindOf'

// 该函数主要负责将多个 reducer 函数结合成一个 reducer 函数并返回出去
export default function combineReducers(reducers) {
  // 获取传入 reducers 对象中子 reducer 对应的 key 并放置在一个数组中
  const reducerKeys = Object.keys(reducers)
  // 该对象用于保存合法有效的 reducer 函数
  const finalReducers = {}
  // 循环遍历对象中的 key
  for (let i = 0; i < reducerKeys.length; i++) {
    const key = reducerKeys[i]

    if (process.env.NODE_ENV !== 'production') {
      // 在开发环境中,如果传入对象的某个键值为 undefined,则打印警告,但不阻塞代码执行
      if (typeof reducers[key] === 'undefined') {
        warning(`No reducer provided for key "${key}"`)
      }
    }

    // 如果对应的键值类型为函数,说明其满足 reducer 的类型,将其 copy 到 finalReducers 对象中
    if (typeof reducers[key] === 'function') {
      finalReducers[key] = reducers[key]
    }
  }
  // 获取 finalReducers 中的所有 key 并放置到一个数组中
  const finalReducerKeys = Object.keys(finalReducers)

  // 存储错误的变量
  let shapeAssertionError
  try {
    /**
     * 循环遍历所有 reducer,检查他们是否符合 redux 要求的规范,这个函数很简单
     * 其主要的工作就是循环遍历 finalReducers 中每一个reducer,传入一个 reducer 中都不可能出现的 action.type
     * 通过检查 reducer 是否具有返回值来判断开发者的 reducer 是否有缺陷,如果有则抛出错误
     */
    assertReducerShape(finalReducers)
  } catch (e) {
    // 将抛出的错误记录下来
    shapeAssertionError = e
  }

  // 返回一个合并好的总 reducer 函数,在这个函数里面会通过闭包大量访问父函数作用域中的变量
  // 可以看到其接受一个旧状态 state 和派发的 action,和我们的子 reducer 形状类似
  return function combination(state = {}, action) {
    // 如果有错误,在第一次 dispatch 的时候就抛出,因为每次 dispatch 之后都会执行该函数
    if (shapeAssertionError) {
      throw shapeAssertionError
    }

    if (process.env.NODE_ENV !== 'production') {
      // 如果是开发环境,则需要检查是否有以下三种情况:
      // 1. 传递的 reducer 是否都无效等情况 
      // 2. 传入的状态是否为对象
      // 3. 传入的状态是否具有 reducers 没有的 key
      // 只要有上述问题的任意一个则打印错误信息,但不阻塞代码执行
      const warningMessage = getUnexpectedStateShapeWarningMessage(
        state,
        finalReducers,
        action,
        unexpectedKeyCache
      )
      if (warningMessage) {
        warning(warningMessage)
      }
    }

    // 标志 state 是否有变化
    let hasChanged = false
    // 下一个 redux 状态
    const nextState = {}
    // 遍历所有的子 reducer
    for (let i = 0; i < finalReducerKeys.length; i++) {
      // 取出每个子 reducer
      const key = finalReducerKeys[i]
      const reducer = finalReducers[key]
      // 取出每个子 reducer 对应的状态值
      const previousStateForKey = state[key]
      // 将该子 reducer 对应的状态值和派发的 action 传入对应的子 reducer 函数
      const nextStateForKey = reducer(previousStateForKey, action)
      // 如果 reducer 被调用后没有返回值,则报错
      if (typeof nextStateForKey === 'undefined') {
        throw new Error('警告...')
      }
      // 将该子 reducer 对应的最新状态更新到 nextState 中
      nextState[key] = nextStateForKey
      // 通过判断上一次 hasChanged 的值和比较新旧子 reducer 对应的状态来判断状态是否真正改变
      // 只要有一个子 reducer 的状态发生改变了就意味着整个 redux 的状态都变了,hasChanged 为 true
      hasChanged = hasChanged || nextStateForKey !== previousStateForKey
    }
    // 如果用户传入的 reducer 有个别无效的,那么就可能会出现 finalReducerKeys 的形状与用户传入 state 的形状不一样,这种情况下 hasChanged 为 true
    hasChanged =
      hasChanged || finalReducerKeys.length !== Object.keys(state).length
    // 如果 hasChanged 为 true,那么 reducer 返回的就是最新的状态,否则依然还是原来的状态
    return hasChanged ? nextState : state
  }
}

如果你使用了 combineReducers 去合并子 reducer,那么 redux 会要求你所管理的状态必须要是对象类型

现在我们可以就回答 combineReducers 是如何将我们所有的子 reducer 合并的了?

每当 dispatch 函数执行时,总 reducer 函数就会被执行,其接收两个参数——旧状态 state 和新派发的 action。总 reducerdispatch 函数里面被执行的时候,首先会根据传入 combineReducers 参数中的 key 来取出所有有效的子 reducer 并执行,每次执行都会接收到该子 reducer 所对应的旧 state 和新派发的 action,通过依次比较每一个子 reducer 的新旧状态是否发生变化来决定总 reducer 是返回最新的 state 还是旧 state

但不知道大家会不会发现一个问题?我们虽然是按照模块划分了多个 reducer,但是每次执行 dispatch 函数的时候所有模块对应的 reducer都会被执行。如果我们开发的是一个很大型的项目,里面有非常多的模块,也就对应着很多的 reducer 函数,一般来说我们要更新的只是某个子 reducer 所对应的状态,但现在的情况是每次执行一次 dispatch 函数都会调用所有的子 reducer,这会不会导致什么性能问题呢?

其实 redux 官网里面是有相关问题的讨论的:redux.js.org/faq/perform…,官方给出的可靠解释大概为:JS 引擎一秒可以执行很多的函数,所以并不会出现性能问题,如果你实在比较在意的话,可以使用一些第三方库

中间件

中间件是用来加强原先的 store的,比如去改造里面的 dispatch 函数或者是 getState 函数,下面我们来看一下没有中间件的 dispatch 是如何更新状态的?

Redux——一个源码比使用简单的库

和我们之前看的源码流程一模一样,那如果我们利用中间件改造了 dispatch 函数之后会发生什么变化呢?

Redux——一个源码比使用简单的库

可以发现,当我们调用 store 中的 dispatch 的时候,并不会立马去执行 reducer 函数,而是先去执行了设置的中间件函数,等到我们想要执行的操作结束了,可以真正更新 redux 状态的时候,我们才会去调用原始的 dispatch 函数去更新 store 中的状态

compose.js

前面简单介绍过 createStore 函数可以接收一个 enhancer 函数来强化 store,当项目中需要多个 enhancer 时,redux 要求我们先用 compose 组装合并之后再传给 createStore。大家应该能猜到 compose 是干什么的了吧?它做的事情就是将多个函数组合成为一个函数,例如 compose(f, g, h) 的返回值是 (...args) => f(g(h(...args))) 这样的一个函数

之前有提过,enhancer 所需的参数是 createStore,其返回值类似于一个加强版的 createStore 函数,那么我们用 compose 组合多个 enhancer 函数的效果到底是怎样的呢?

比如说现在有三个 enahncer 函数:ABC,它们作为参数传递给了 compose 函数,如果执行组合好的函数的话,比如 compose(A,B,C)(createStore),从上面的示例中可以看出,C 函数会最先被执行,其接收最原始的 createStore 函数,返回一个加强版的 createStore,即A(createStore),然后该加强版的 createStore 会传递给 B 函数执行,即 B(A(createStore)),其返回值也是一个加强版的 createStore 函数,紧接着以同样的方式调用了 C(B(A(createStore))),这就是强化过后的 createStore 函数

// 返回一个通过组合传递进来的参数所得的函数
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)))
}

applyMiddleware.js

中间件内部做的就是加强 store 的工作,一般都是去改造 store中原始的dispatch 方法,但中间件并不满足 enahncer 函数的形式,所以不能作为参数直接传入 createStore。所以applyMiddleware 就派上用场了,它就是为了将中间件包装成一个 enhancer 而出现的,下面是一个使用 applyMiddleware 的示例:

import { createStore, compose, applyMiddleware } from "redux";
import thunk from "redux-thunk";
import reducer from './reducer';

// thunk 是一个中间件,而 applyMiddleware(thunk) 就是将该中间件改造成 enahncer
// compose 是为了将多个 enhancer 合并在一起,因为我们不能确保以后项目中没有其它的 enahncer,所以一般都会包裹一层
const store = createStore(reducer, compose(
  applyMiddleware(thunk)
));

export default store;
// 该函数接受中间件数组并返回 createStore 所需要的 enhancer 函数
export default function applyMiddleware(...middlewares) {
  // 该返回的函数就是 enhancer 函数,其会被 enhancer(createStore)(reducer, preloadedState) 这样调用,所以其需要有一个参数去接受 createStore 函数
  return (createStore) => (...args) => {
    // 利用 createStore 函数和传递进来的 reducer、preloadedState 创建一个 store
    const store = createStore(...args)
    // 一个临时的dispatch,作用是在 dispatch 改造完成前调用会打印错误信息
    let dispatch = () => {
      throw new Error('警告...')
    }

    // 定义了中间件的一些 api,getState 依然还是 store 中的 getState方法
    const middlewareAPI = {
      getState: store.getState,
      dispatch: (...args) => dispatch(...args),
    }
    // middlewares 是传入的中间件数组,中间件函数的返回值是一个改造 dispatch 的函数
    // 调用数组中的每个中间件函数,得到所有的改造函数,从调用中间件函数的形式我们也能猜到其必须要接收一个含有 dispatch 和 getState 方法的对象
    const chain = middlewares.map((middleware) => middleware(middlewareAPI))
    // 既然是改造 dispatch 的函数,那么返回的新 dispatch 函数肯定要依赖于原始的 dispatch 函数,所以我们这里要将最原始的 dispatch 函数传入
    // 通过 compose 函数,将改造后的 dispatch 函数传递给下一个改造函数进行改造,直到拿到最终改造好的 disaptch 函数
    // 构建了一个新的 dispatch 函数,这个就是开发者在外面所使用的那个 dispatch
    dispatch = compose(...chain)(store.dispatch)

    // 该函数像 createStore 一样会返回了一个 store,但其改造了原始的 dispatch 函数,使得 store.dispatch 函数更加强大
    return {
      ...store,
      dispatch,
    }
  }
}

redux-thunk

如果要说 redux 中比较具有代表性的中间件,redux-thunk 一定有话语权,因为它的源码会让你很吃惊,因为它只有大概 10 行左右。我们先来说一下它的作用:正常的 dispatch 函数的参数 action 应该是一个纯对象,但使用 redux-thunk 加强过的 dispatch 的参数可以是一个函数,下面有个示例:

function logStateInOneSecond() {
    // 这个返回的函数会在合适的时候 dispatch 一个真正的 action
    return (dispatch, getState) => {  
        setTimeout({
            console.log(getState())
            dispatch({
                type:'LOG_OK',
                info: {
                    name: 'myName',
                }
            })
        }, 1000)
    }
}
// logStateInOneSecond() 的值是一个函数,但可以传入到 redux-thunk 改造过的 dispatch 函数中
store.dispatch(logStateInOneSecond()) 

下面是 redux-thunk 的源码:

function createThunkMiddleware(extraArgument) {
  // 该函数的返回值就是外界所引入的 redux-thunk 中间件,看了 applyMiddleware 的源码之后我们会发现它就是一个标准的 middleware 函数
  // 其接受一个对象,其中包含 dispatch 和 getState 方法,它的返回值就是一个改造 dispatch 的函数
  // 还记得在 applyMiddleware 中会执行 compose 函数吗?—— compose(...chain)(store.dispatch)
  // next 就是接收即将要改造的 disaptch 的参数,下面高亮部分也就是以 action 为参数的函数就是改造过后的 dispatch 函数
  return ({ dispatch, getState }) => next => action => {
 // 可以看到如果 action 为函数类型,则不直接使用原 dispatch 派发 action
 // 而是调用该函数并将原 dispatch 和 getState 传入进去,getState  前面有讲过就是最原始的 store.getState 函数而已
 if ( typeof action === 'function' ) {
 return action(dispatch, getState, extraArgument );
}

 // 如果不是一个函数类型,那么就直接用原 dispatch 进行派发操作即可
 return next(action);
};
}

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

export default thunk;

总结

redux 的源码与一些框架相比,比如 react ,会显得简单很多,但并不代表里面没有细节存在。相反,我觉得 redux 在细节方面做的很到位,比如 redux 通过 isDispatching 这个变量来防止开发者在 reducer 中订阅/取消订阅或重复进行 dispatch,通过 ensureCanMutateNextListeners 函数来确保开发者就算是在监听器中订阅/取消订阅了其它的监听器,当前 dispatch 过程中遍历的监听器数组依然不会受到影响,这都是 redux 在细节上的一些处理

看完了源码之后,也算是给了自己一点信心,在下次使用 redux 开发项目的时候能够更加自信,推荐大家有时间的话可以尝试自己阅读一遍它的源码,无论是对自己的 JS 水平和思维都有一定的提升。