Redux 源码解读 —— 从源码开始学 Redux

栏目: 服务器 · 发布时间: 5年前

内容简介:已经快一年没有碰过 React 全家桶了,最近换了个项目组要用到 React 技术栈,所以最近又复习了一下;捡起旧知识的同时又有了一些新的收获,在这里作文以记之。在阅读文章之前,最好已经知道如何使用 Redux(不是 React-Redux)。为了更好的解读源码,我们可以把源码拷贝到本地,然后搭建一个开发环境。Redux 的使用不依赖于 React,所以你完全可以在一个极为简单的 JavaScript 项目中使用它。这里不再赘述开发环境的搭建过程,需要的同学可以直接拷贝我的代码到本地,然后安装依赖,运行项

已经快一年没有碰过 React 全家桶了,最近换了个项目组要用到 React 技术栈,所以最近又复习了一下;捡起旧知识的同时又有了一些新的收获,在这里作文以记之。

在阅读文章之前,最好已经知道如何使用 Redux(不是 React-Redux)。

一、准备环境

为了更好的解读源码,我们可以把源码拷贝到本地,然后搭建一个开发环境。Redux 的使用不依赖于 React,所以你完全可以在一个极为简单的 JavaScript 项目中使用它。这里不再赘述开发环境的搭建过程,需要的同学可以直接拷贝我的代码到本地,然后安装依赖,运行项目。

$ git clone https://github.com/zhongdeming428/redux && cd redux

$ npm i

$ npm run dev
复制代码

二、阅读源码

(1)源代码结构

忽略项目中的那些说明文档什么的,只看 src 这个源文件目录,其结构如下:

src
├── applyMiddleware.js  // 应用中间件的 API
├── bindActionCreators.js   // 转换 actionCreators 的 API
├── combineReducers.js  // 组合转换 reducer 的 API
├── compose.js  // 工具函数,用于嵌套调用中间件
├── createStore.js  // 入口函数,创建 store 的 API
├── index.js    // redux 项目的入口文件,用于统一暴露所有 API
├── test
│   └── index.js    // 我所创建的用于调试的脚本
└── utils   // 专门放 工具 函数的目录
    ├── actionTypes.js  // 定义了一些 redux 预留的 action type
    ├── isPlainObject.js  // 用于判断是否是纯对象 
    └── warning.js  // 用于抛出合适的警告信息

复制代码

可以看出来 redux 的源码结构简单清晰明了,几个主要的(也是仅有的) API 被尽可能的分散到了单个的文件模块中,我们只需要挨个的看就行了。

(2)index.js

上一小节说到 index.js 是 redux 项目的入口文件,用于暴露所有的 API,所以我们来看看代码:

import createStore from './createStore'
import combineReducers from './combineReducers'
import bindActionCreators from './bindActionCreators'
import applyMiddleware from './applyMiddleware'
import compose from './compose'
import warning from './utils/warning'
import __DO_NOT_USE__ActionTypes from './utils/actionTypes'
// 不同的 API 写在不同的 js 文件中,最后通过 index.js 统一导出。

// 这个函数用于判断当前代码是否已经被打包工具(比如 Webpack)压缩过,如果被压缩过的话,
// isCrushed 函数的名称会被替换掉。如果被替换了函数名但是 process.env.NODE_ENV 又不等于 production
// 的时候,提醒用户使用生产环境下的精简代码。
function isCrushed() {}

if (
  process.env.NODE_ENV !== 'production' &&
  typeof isCrushed.name === 'string' &&
  isCrushed.name !== 'isCrushed'
) {
  warning(
    'You are currently using minified code outside of NODE_ENV === "production". ' +
      'This means that you are running a slower development build of Redux. ' +
      'You can use loose-envify (https://github.com/zertosh/loose-envify) for browserify ' +
      'or setting mode to production in webpack (https://webpack.js.org/concepts/mode/) ' +
      'to ensure you have the correct code for your production build.'
  )
}

// 导出主要的 API。
export {
  createStore,
  combineReducers,
  bindActionCreators,
  applyMiddleware,
  compose,
  __DO_NOT_USE__ActionTypes
}
复制代码

我删除了所有的英文注释以减小篇幅,如果大家想看原来的注释,可以去 redux 的项目查看源码。

可以看到在程序的头部引入了所有的 API 模块以及工具函数,然后在底部统一导出了。这一部分比较简单,主要是 isCrushed 函数有点意思。作者用这个函数来判断代码是否被压缩过(判断函数名是否被替换掉了)。

这一部分也引用到了工具函数,由于这几个函数比较简单,所以可以先看看它们是干嘛的。

(3)工具函数

除了 compose 函数以外,所有的工具函数都被放在了 utils 目录下。

actionTypes.js

这个工具模块定义了几种 redux 预留的 action type,包括 reducer 替换类型、reducer 初始化类型和随机类型。下面上源码:

// 定义了一些 redux 保留的 action type。
// 随机字符串确保唯一性。
const randomString = () =>
  Math.random()
    .toString(36)
    .substring(7)
    .split('')
    .join('.')

const ActionTypes = {
  INIT: `@@redux/INIT${randomString()}`,
  REPLACE: `@@redux/REPLACE${randomString()}`,
  PROBE_UNKNOWN_ACTION: () => `@@redux/PROBE_UNKNOWN_ACTION${randomString()}`
}

export default ActionTypes
复制代码

可以看出就是返回了一个 ActionTypes 对象,里面包含三种类型:INIT、REPLACE 和 PROBE_UNKNOW_ACTION。分别对应之前所说的几种类型,为了防止和用户自定义的 action type 相冲突,刻意在 type 里面加入了随机值。在后面的使用中,通过引入 ActionType 对象来进行对比。

isPlainObject.js

这个函数用于判断传入的对象是否是纯对象,因为 redux 要求 action 和 state 是一个纯对象,所以这个函数诞生了。

上源码:

/**
 * 判断一个参数是否是纯对象,纯对象的定义就是它的构造函数为 Object。
 * 比如: { name: 'isPlainObject', type: 'funciton' }。
 * 而 isPlainObject 这个函数就不是纯对象,因为它的构造函数是 Function。
 * @param {any} obj 要检查的对象。
 * @returns {boolean} 返回的检查结果,true 代表是纯对象。
 */
export default function isPlainObject(obj) {
  if (typeof obj !== 'object' || obj === null) return false

  let proto = obj
  // 获取最顶级的原型,如果就是自身,那么说明是纯对象。
  while (Object.getPrototypeOf(proto) !== null) {
    proto = Object.getPrototypeOf(proto)
  }

  return Object.getPrototypeOf(obj) === proto
}
复制代码

warning.js

这个函数用于抛出适当的警告,没啥好说的。

/**
 * Prints a warning in the console if it exists.
 *
 * @param {String} message The warning message.
 * @returns {void}
 */
export default function warning(message) {
  /* eslint-disable no-console */
  if (typeof console !== 'undefined' && typeof console.error === 'function') {
    console.error(message)
  }
  /* eslint-enable no-console */
  try {
    // This error was thrown as a convenience so that if you enable
    // "break on all exceptions" in your console,
    // it would pause the execution at this line.
    throw new Error(message)
  } catch (e) {} // eslint-disable-line no-empty
}
复制代码

compose.js

这个函数用于嵌套调用中间件(middleware),进行初始化。

/**
 * 传入一系列的单参数函数作为参数(funcs 数组),返回一个新的函数,这个函数可以接受
 * 多个参数,运行时会将 funcs 数组中的函数从右至左进行调用。
 * @param {...Function} funcs 一系列中间件。
 * @returns {Function} 返回的结果函数。
 * 从右至左调用,比如: compose(f, g, h) 将会返回一个新函数
 * (...args) => f(g(h(...args))).
 */
export default function compose(...funcs) {
  if (funcs.length === 0) {
    return arg => arg
  }

  if (funcs.length === 1) {
    return funcs[0]
  }
  // 通过 reduce 方法迭代。
  return funcs.reduce((a, b) => (...args) => a(b(...args)))
}
复制代码

(4)createStore.js

看完了工具函数和入口函数,接下来就要正式步入主题了。我们使用 redux 的重要一步就是通过 createStore 方法创建 store。那么接下来看看这个方法是怎么创建 store 的,store 又是个什么东西呢?

我们看源码:

import $$observable from 'symbol-observable'
// 后面会讲。
import ActionTypes from './utils/actionTypes'
// 引入一些预定义的保留的 action type。
import isPlainObject from './utils/isPlainObject'
// 判断一个对象是否是纯对象。

// 使用 redux 最主要的 API,就是这个 createStore,它用于创建一个 redux store,为你提供状态管理。
// 它接受三个参数(第二三个可选),第一个是 reducer,用于改变 redux store 的状态;第二个是初始化的 store,
// 即最开始时候 store 的快照;第三个参数是由 applyMiddleware 函数返回的 enhancer 对象,使用中间件必须
// 提供的参数。
export default function createStore(reducer, preloadedState, enhancer) {
  // 下面这一段基本可以不看,它们是对参数进行适配的。
  /*************************************参数适配****************************************/
  if (
    (typeof preloadedState === 'function' && typeof enhancer === 'function') ||
    (typeof enhancer === 'function' && typeof arguments[3] === 'function')
  ) {
    // 如果传递了多个 enhancer,抛出错误。
    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'
    )
  }
  // 如果没有传递默认的 state(preloadedState 为函数类型,enhancer 为未定义类型),那么传递的
  // preloadedState 即为 enhancer。
  if (typeof preloadedState === 'function' && typeof enhancer === 'undefined') {
    enhancer = preloadedState
    preloadedState = undefined
  }

  if (typeof enhancer !== 'undefined') {
    if (typeof enhancer !== 'function') {
      // 如果 enhancer 为不为空且非函数类型,报错。
      throw new Error('Expected the enhancer to be a function.')
    }
    // 使用 enhancer 对 createStore 进行处理,引入中间件。注意此处没有再传递 enhancer 作为参数。实际上 enhancer 会对 createStore 进行处理,然后返回一个实际意义上的 createStore 用于创建 store 对象,参考 applyMiddleware.js。
    return enhancer(createStore)(reducer, preloadedState)
  }
  // 如果 reducer 不是函数类型,报错。
  if (typeof reducer !== 'function') {
    throw new Error('Expected the reducer to be a function.')
  }
  /*********************************************************************************/

  // 在函数内部定义一系列局部变量,用于存储数据。
  let currentReducer = reducer  // 存储当前的 reducer。
  let currentState = preloadedState // 用于存储当前的 store,即为 state。  
  let currentListeners = [] // 用于存储通过 store.subscribe 注册的当前的所有订阅者。
  let nextListeners = currentListeners  // 新的 listeners 数组,确保不直接修改 listeners。
  let isDispatching = false // 当前状态,防止 reducer 嵌套调用。

  // 顾名思义,确保 nextListeners 可以被修改,当 nextListeners 与 currentListeners 指向同一个数组的时候
  // 让 nextListeners 成为 currentListeners 的副本。防止修改 nextListeners 导致 currentListeners 发生变化。
  // 一开始我也不是很明白为什么会存在 nextListeners,因为后面 dispatch 函数中还是直接把 nextListeners 赋值给了 currentListeners。
  // 直接使用 currentListeners 也是可以的。后来去 redux 的 repo 搜了搜,发现了一个 issue(https://github.com/reduxjs/redux/issues/2157) 讲述了这个做法的理由。
  // 提交这段代码的作者的解释(https://github.com/reduxjs/redux/commit/c031c0a8d900e0e95a4915ecc0f96c6fe2d6e92b)是防止 Array.slice 的滥用,只有在必要的时候调用 Array.slice 方法来复制 listeners。
  // 以前的做法是每次 dispatch 都要 slice 一次,导致了性能的降低吧。
  function ensureCanMutateNextListeners() {
    if (nextListeners === currentListeners) {
      nextListeners = currentListeners.slice()
    }
  }

  // 返回 currentState,即 store 的快照。
  function getState() {
    // 防止在 reducer 中调用该方法,reducer 会接受 state 参数。
    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
  }

  // store.subscribe 函数,订阅 dispatch。
  function subscribe(listener) {
    if (typeof listener !== 'function') {
      throw new Error('Expected the listener to be a function.')
    }
    // 不允许在 reducer 中进行订阅。
    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#subscribe(listener) for more details.'
      )
    }

    let isSubscribed = true
    // 每次操作 nextListeners 之前先确保可以修改。
    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 https://redux.js.org/api-reference/store#subscribe(listener) for more details.'
        )
      }

      isSubscribed = false
      // 每次操作 nextListeners 之前先确保可以修改。
      ensureCanMutateNextListeners()
      const index = nextListeners.indexOf(listener)
      nextListeners.splice(index, 1)
    }
  }

  // store.dispatch 函数,用于触发 reducer 修改 state。
  function dispatch(action) {
    if (!isPlainObject(action)) {
      // action 必须是纯对象。
      throw new Error(
        'Actions must be plain objects. ' +
          'Use custom middleware for async actions.'
      )
    }

    if (typeof action.type === 'undefined') {
      // 每个 action 必须包含一个 type 属性,指定修改的类型。
      throw new Error(
        'Actions may not have an undefined "type" property. ' +
          'Have you misspelled a constant?'
      )
    }
    // reducer 内部不允许派发 action。
    if (isDispatching) {
      throw new Error('Reducers may not dispatch actions.')
    }

    try {
      // 调用 reducer 之前,先将标志位置一。
      isDispatching = true
      // 调用 reducer,返回的值即为最新的 state。
      currentState = currentReducer(currentState, action)
    } finally {
      // 调用完之后将标志位置 0,表示 dispatch 结束。
      isDispatching = false
    }

    // dispatch 结束之后,执行所有订阅者的函数。
    const listeners = (currentListeners = nextListeners)
    for (let i = 0; i < listeners.length; i++) {
      const listener = listeners[i]
      listener()
    }

    // 返回当前所使用的 action,这一步是中间件嵌套使用的关键,很重要。
    return action
  }

  // 一个比较新的 API,用于动态替换当前的 reducers。适用于按需加载,代码拆分等场景。
  function replaceReducer(nextReducer) {
    if (typeof nextReducer !== 'function') {
      throw new Error('Expected the nextReducer to be a function.')
    }
    // 执行默认的 REPLACE 类型的 action。在 combineReducers 函数中有使用到这个类型。
    currentReducer = nextReducer
    dispatch({ type: ActionTypes.REPLACE })
  }

  // 这是为了适配 ECMA TC39 会议的一个有关 Observable 的提案(参考https://github.com/tc39/proposal-observable)所写的一个函数。
  // 作用是订阅 store 的变化,适用于所有实现了 Observable 的类库(主要是适配 RxJS)。
  // 我找到了引入这个功能的那个 commit:https://github.com/reduxjs/redux/pull/1632。
  function observable() {
    // outerSubscribe 即为外部的 subscribe 函数。
    const outerSubscribe = subscribe
    // 返回一个纯对象,包含 subscribe 方法。
    return {
      subscribe(observer) {
        if (typeof observer !== 'object' || observer === null) {
          throw new TypeError('Expected the observer to be an object.')
        }

        // 用于给 subscribe 注册的函数,严格按照 Observable 的规范实现,observer 必须有一个 next 属性。
        function observeState() {
          if (observer.next) {
            observer.next(getState())
          }
        }

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

      // $$observable 即为 Symbol.observable,也属于 Observable 的规范,返回自身。
      [$$observable]() {
        return this
      }
    }
  }
  // 初始化时 dispatch 一个 INIT 类型的 action,校验各种情况。
  dispatch({ type: ActionTypes.INIT })

  // 返回一个 store 对象。
  return {
    dispatch,
    subscribe,
    getState,
    replaceReducer,
    [$$observable]: observable
  }
}
复制代码

不难发现,我们的 store 对象就是一个纯 JavaScript 对象。包含几个属性 API,而我们的 state 就存储在 createStore 这个方法内部,是一个局部变量,只能通过 getState 方法访问到。这里实际上是对闭包的利用,所有我们操作的 state 都存储在 getState 方法内部的一个变量里面。直到我们的程序结束(或者说 store 被销毁),createStore 方法才会被回收,里面的变量才会被销毁。

而 subscribe 方法就是对观察者模式的利用(注意不是发布订阅模式,二者有区别,不要混淆),我们通过 subscribe 方法注册我们的函数,我们的函数会给存储到 createStore 方法的一个局部变量当中,每次 dispatch 被调用之后,都会遍历一遍 currentListeners,依次执行其中的方法,达到我们订阅的要求。

(5)combineReducers.js

了解了 createStore 到底是怎么一回事,我们再来看看 combineReducers 到底是怎么创建 reducer 的。

我们写 reducer 的时候,实际上是在写一系列函数,然后整个到一个对象的属性上,最后传给 combineReducers 进行处理,处理之后就可以供 createStore 使用了。

例如:

// 创建我们的 reducers。
const _reducers = {
  items(items = [], { type, payload }) {
    if (type === 'ADD_ITEMS') items.push(payload);
    return items;
  },
  isLoading(isLoading = false, { type, payload }) {
    if (type === 'IS_LOADING') return true;
    return false;
  }
};
// 交给 combineReducers 处理,适配 createStore。
const reducers = combineReducers(_reducers);
// createStore 接受 reducers,创建我们需要的 store。
const store = createStore(reducers);
复制代码

那么 combineReducers 对我们的 reducers 对象进行了哪些处理呢?

下面的代码比较长,希望大家能有耐心。

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

/**
 * 用于获取错误信息的工具函数,如果调用你所定义的某个 reducer 返回了 undefined,那么就调用这个函数
 * 抛出合适的错误信息。
 * 
 * @param {String} key 你所定义的某个 reducer 的函数名,同时也是 state 的一个属性名。
 * 
 * @param {Object} action 调用 reducer 时所使用的 action。
 */
function getUndefinedStateErrorMessage(key, action) {
  const actionType = action && action.type
  const actionDescription =
    (actionType && `action "${String(actionType)}"`) || 'an action'

  return (
    `Given ${actionDescription}, reducer "${key}" returned undefined. ` +
    `To ignore an action, you must explicitly return the previous state. ` +
    `If you want this reducer to hold no value, you can return null instead of undefined.`
  )
}

/**
 * 工具函数,用于校验未知键,如果 state 中的某个属性没有对应的 reducer,那么返回报错信息。
 * 对于 REPLACE 类型的 action type,则不进行校验。
 * @param {Object} inputState 
 * @param {Object} reducers 
 * @param {Object} action 
 * @param {Object} unexpectedKeyCache 
 */
function getUnexpectedStateShapeWarningMessage(
  inputState,
  reducers,
  action,
  unexpectedKeyCache
) {
  const reducerKeys = Object.keys(reducers)
  const argumentName =
    action && action.type === ActionTypes.INIT
      ? 'preloadedState argument passed to createStore'
      : 'previous state received by the reducer'

  // 如果 reducers 长度为 0,返回对应错误信息。
  if (reducerKeys.length === 0) {
    return (
      'Store does not have a valid reducer. Make sure the argument passed ' +
      'to combineReducers is an object whose values are reducers.'
    )
  }

  if (!isPlainObject(inputState)) {
    return (
      `The ${argumentName} has unexpected type of "` +
      {}.toString.call(inputState).match(/\s([a-z|A-Z]+)/)[1] +             // {}.toString.call(inputState).match(/\s([a-z|A-Z]+)/)[1]
      `". Expected argument to be an object with the following ` +          // 返回的是 inputState 的类型。
      `keys: "${reducerKeys.join('", "')}"`
    )
  }

  // 获取所有 State 有而 reducers 没有的属性,加入到 unexpectedKeysCache。
  const unexpectedKeys = Object.keys(inputState).filter(
    key => !reducers.hasOwnProperty(key) && !unexpectedKeyCache[key]
  )

  // 加入到 unexpectedKeyCache。
  unexpectedKeys.forEach(key => {
    unexpectedKeyCache[key] = true
  })

  // 如果是 REPLACE 类型的 action type,不再校验未知键,因为按需加载的 reducers 不需要校验未知键,现在不存在的 reducers 可能下次就加上了。
  if (action && action.type === ActionTypes.REPLACE) return

  if (unexpectedKeys.length > 0) {
    return (
      `Unexpected ${unexpectedKeys.length > 1 ? 'keys' : 'key'} ` +
      `"${unexpectedKeys.join('", "')}" found in ${argumentName}. ` +
      `Expected to find one of the known reducer keys instead: ` +
      `"${reducerKeys.join('", "')}". Unexpected keys will be ignored.`
    )
  }
}

/**
 * 用于校验所有 reducer 的合理性:传入任意值都不能返回 undefined。
 * @param {Object} reducers 你所定义的 reducers 对象。
 */
function assertReducerShape(reducers) {
  Object.keys(reducers).forEach(key => {
    const reducer = reducers[key]
    // 获取初始化时的 state。
    const initialState = reducer(undefined, { type: ActionTypes.INIT })

    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.INIT 的操作。这时候重新用随机值校验。
    // 如果返回 undefined,说明用户可能对 INIT 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.`
      )
    }
  })
}

// 把你所定义的 reducers 对象转化为一个庞大的汇总函数。
// 可以看出,combineReducers 接受一个 reducers 对象作为参数,
// 然后返回一个总的函数,作为最终的合法的 reducer,这个 reducer 
// 接受 action 作为参数,根据 action 的类型遍历调用所有的 reducer。
export default function combineReducers(reducers) {
  // 获取 reducers 所有的属性名。
  const reducerKeys = Object.keys(reducers)
  const finalReducers = {}
  // 遍历 reducers 的所有属性,剔除所有不合法的 reducer。
  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') {
      // 将 reducers 中的所有 reducer 拷贝到新的 finalReducers 对象上。
      finalReducers[key] = reducers[key]
    }
  }
  // finalReducers 是一个纯净的经过过滤的 reducers 了,重新获取所有属性名。
  const finalReducerKeys = Object.keys(finalReducers)

  let unexpectedKeyCache
  // unexpectedKeyCache 包含所有 state 中有但是 reducers 中没有的属性。
  if (process.env.NODE_ENV !== 'production') {
    unexpectedKeyCache = {}
  }

  let shapeAssertionError
  try {
    // 校验所有 reducer 的合理性,缓存错误。
    assertReducerShape(finalReducers)
  } catch (e) {
    shapeAssertionError = e
  }

  // 这就是返回的新的 reducer,一个纯函数。每次 dispatch 一个 action,都要执行一遍 combination 函数,
  // 进而把你所定义的所有 reducer 都执行一遍。
  return function combination(state = {}, action) {
    if (shapeAssertionError) {
      // 如果有缓存的错误,抛出。
      throw shapeAssertionError
    }

    if (process.env.NODE_ENV !== 'production') {
      // 非生产环境下校验 state 中的属性是否都有对应的 reducer。
      const warningMessage = getUnexpectedStateShapeWarningMessage(
        state,
        finalReducers,
        action,
        unexpectedKeyCache
      )
      if (warningMessage) {
        warning(warningMessage)
      }
    }

    let hasChanged = false
    // state 是否改变的标志位。
    const nextState = {}
    // reducer 返回的新 state。
    for (let i = 0; i < finalReducerKeys.length; i++) {   // 遍历所有的 reducer。
      const key = finalReducerKeys[i]  // 获取 reducer 名称。
      const reducer = finalReducers[key]  // 获取 reducer。
      const previousStateForKey = state[key]  // 旧的 state 值。
      const nextStateForKey = reducer(previousStateForKey, action)  // 执行 reducer 返回的新的 state[key] 值。
      if (typeof nextStateForKey === 'undefined') {
        // 如果经过了那么多校验,你的 reducer 还是返回了 undefined,那么就要抛出错误信息了。
        const errorMessage = getUndefinedStateErrorMessage(key, action)
        throw new Error(errorMessage)
      }
      nextState[key] = nextStateForKey
      // 把返回的新值添加到 nextState 对象上,这里可以看出来,你所定义的 reducer 的名称就是对应的 state 的属性,所以 reducer 命名要规范!
      hasChanged = hasChanged || nextStateForKey !== previousStateForKey
      // 检验 state 是否发生了变化。
    }
    // 根据标志位返回对应的 state。
    return hasChanged ? nextState : state
  }
}
复制代码

(6)applyMiddleware.js

combineReducers 方法代码比较多,但是实际逻辑还是很简单的,接下来这个函数代码不多,但是逻辑要稍微复杂一点,它就是应用中间件的 applyMiddleware 函数。这个函数的思想比较巧妙,值得学习。

import compose from './compose'

// 用于应用中间件的函数,可以同时传递多个中间件。中间件的标准形式为:
//  const middleware = store => next => action => { /*.....*/ return next(action); }
export default function applyMiddleware(...middlewares) {
  // 返回一个函数,接受 createStore 作为参数。args 参数即为 reducer 和 preloadedState。
  return createStore => (...args) => {
    // 在函数内部调用 createStore 创建一个 store 对象,这里不会传递 enhancer,因为 applyMiddleware 本身就是在创建一个 enhancer,然后给 createStore 调用。
    // 这里实际上是通过 applyMiddleware 把 store 的创建推迟了。为什么要推迟呢?因为要利用 middleWares 做文章,先初始化中间件,重新定义 dispatch,然后再创建 store,这时候创建的 store 所包含的 dispatch 方法就区别于不传递 enhancer 时所创建的 dispatch 方法了,其中包含了中间件所定义的一些逻辑,这就是为什么中间件可以干预 dispatch 的原因。
    const store = createStore(...args)
    // 这里对 dispatch 进行了重新定义,不管传入什么参数,都会报错,这样做的目的是防止你的中间件在初始化的时候就
    // 调用 dispatch。
    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) // 注意最后 dispatch 的时候不会访问上面报错的那个 dispatch 函数了,因为那个函数被下面的 dispatch 覆盖了。
    }
    // 对于每一个 middleware,都传入 middlewareAPI 进行调用,这就是中间件的初始化。
    // 初始化后的中间件返回一个新的函数,这个函数接受 store.dispatch 作为参数,返回一个替换后的 dispatch,作为新的
    // store.dispatch。
    const chain = middlewares.map(middleware => middleware(middlewareAPI))
    // compose 方法把所有中间件串联起来调用。用最终结果替换 dispatch 函数,之后所使用的所有 store.dispatch 方法都已经是
    // 替换了的,加入了新的逻辑。
    dispatch = compose(...chain)(store.dispatch)
    // 初始化中间件以后,把报错的 dispatch 函数覆盖掉。

    /**
     * middle 的标准形式:
     * const middleware = ({ getState, dispatch }) => next => action => {
     *    // ....
     *    return next(action);
     * }
     * 这里 next 是经过上一个 middleware 处理了的 dispatch 方法。
     * next(action) 返回的仍然是一个 dispatch 方法。
     */

    return {
      ...store,
      dispatch  // 全新的 dispatch。
    }
  }
}
复制代码

代码量真的很少,但是真的很巧妙,这里有几点很关键:

  • compose 方法利用 Array.prototype.reduce 实现中间件的嵌套调用,返回一个全新的函数,可以接受新的参数(上一个中间件处理过的 dispatch),最终返回一个全新的包含新逻辑的 dispatch 方法。你看 middleware 经过初始化后返回的函数的格式:

    next => action => {
      return next(action);
    }
    复制代码

    其中 next 可以看成 dispatch ,这不就是接受一个 dispatch 作为参数,然后返回一个新的 dispatch 方法吗?原因就是我们可以认为接受 action 作为参数,然后触发 reducer 更改 state 的所有函数都是 dispatch 函数。

  • middleware 中间件经过初始化以后,返回一个新函数,它接受 dispatch 作为参数,然后返回一个新的 dispatch 又可以供下一个 middleware 调用,这就导致所有的 middleware 可以嵌套调用了!而且最终返回的结果也是一个 dispatch 函数。

    最终得到的 dispatch 方法,是把原始的 store.dispatch 方法传递给最后一个 middleware,然后层层嵌套处理,最后经过第一个 middleware 处理过以后所返回的方法。所以我们在调用应用了中间件的 dispatch 函数时,从左至右的经过了 applyMiddleware 方法的所有参数(middleware)的处理。这有点像是包裹和拆包裹的过程。

  • 为什么 action 可以经过所有的中间件处理呢?我们再来看看中间件的基本结构:

    ({ dispatch, getState }) => next => action => {
      return next(action);
    }
    复制代码

    我们可以看到 action 进入函数以后,会经过 next 的处理,并且会返回结果。next 会返回什么呢?因为第一个 next 的值就是 store.dispatch,所以看看 store.dispatch 的源码就知道了。

    function dispatch(action) {
      // 省略了一系列操作的代码……
    
      // 返回当前所使用的 action,这一步是中间件嵌套使用的关键。
      return action
    }
    复制代码

    没错,store.dispatch 最终返回了 action,由于中间件嵌套调用,所以每个 next 都返回 action,然后又可以供下一个 next 使用,环环相扣,十分巧妙。

这部分描述的有点拗口,语言捉急但又不想画图,各位还是自己多想想好了。

(7)bindActionCreators.js

这个方法没有太多好说的,主要作用是减少大家 dispatch reducer 所要写的代码,比如你原来有一个 action:

const addItems = item => ({
  type: 'ADD_ITEMS',
  payload: item
});
复制代码

然后你要调用它的时候:

store.dispatch(addItems('item value'));
复制代码

如果你使用 bindActionCreators:

const _addItems = bindActionCreators(addItems, store.dispatch);
复制代码

当你要 dispatch reducer 的时候:

_addItems('item value');
复制代码

这样就减少了你要写的重复代码,另外你还可以把所有的 action 写在一个对象里面传递给 bindActionCreators,就像传递给 combineReducers 的对象那样。

下面看看源码:

/**
 * 该函数返回一个新的函数,调用新的函数会直接 dispatch ActionCreator 所返回的 action。
 * 这个函数是 bindActionCreators 函数的基础,在 bindActionCreators 函数中会把 actionCreators 拆分成一个一个
 * 的 ActionCreator,然后调用 bindActionCreator 方法。
 * @param {Function} actionCreator 一个返回 action 纯对象的函数。
 * @param {Function} dispatch store.dispatch 方法,用于触发 reducer。
 */
function bindActionCreator(actionCreator, dispatch) {
  return function() {
    return dispatch(actionCreator.apply(this, arguments))
  }
}

// 接受一个 actionCreator(或者一个 actionCreators 对象)和一个 dispatch 函数作为参数,
//  然后返回一个函数或者一个对象,直接执行这个函数或对象中的函数可以让你不必再调用 dispatch。
export default function bindActionCreators(actionCreators, dispatch) {
  // 如果 actionCreators 是一个函数而非对象,那么直接调用 bindActionCreators 方法进行转换,此时返回
  // 结果也是一个函数,执行这个函数会直接 dispatch 对应的 action。
  if (typeof actionCreators === 'function') {
    return bindActionCreator(actionCreators, dispatch)
  }

  // actionCreators 既不是函数也不是对象,或者为空时,抛出错误。
  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"?`
    )
  }

  // 如果 actionCreators 是一个对象,那么它的每一个属性就应该是一个 actionCreator,遍历每一个 actionCreator,
  // 使用 bindActionCreator 进行转换。
  const keys = Object.keys(actionCreators)
  const boundActionCreators = {}
  for (let i = 0; i < keys.length; i++) {
    const key = keys[i]
    const actionCreator = actionCreators[key]
    // 把转换结果绑定到 boundActionCreators 对象,最后会返回它。
    if (typeof actionCreator === 'function') {
      boundActionCreators[key] = bindActionCreator(actionCreator, dispatch)
    }
  }
  return boundActionCreators
}
复制代码

这部分挺简单的,主要作用在于把 action creator 转化为可以直接使用的函数。

三、中间件

看了源码以后,觉得中间件并没有想象中的那么晦涩难懂了。就是一个基本的格式,然后你在你的中间件里面可以为所欲为,最后调用固定的方法,返回固定的内容就完事了。这就是为什么大多数 redux middleware 的源码都很短小精悍的原因。

看看 redux-thunk 的源码:

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

    return next(action);
  };
}

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

export default thunk;
复制代码

是不是很短很小?那么到底干了什么让它这么受欢迎呢?

实际上 redux-thunk 可以被认为就是:

// 这就是典型的 middleware 格式。
({ dispatch, getState }) => next => action => {
  // next 就是 dispatch 方法。注释所在的函数就是返回的新的 dispatch。
  // 先判断一下 action 是不是一个函数。
  if (typeof action === 'function') {
    // 如果是函数,调用它,传递 dispatch,getState 和 多余的参数作为 aciton 的参数。
    return action(dispatch, getState, extraArgument);
  }
  // 如果 action 不是函数,直接 nextr调用 action,返回结果就完事儿了。
  return next(action);
};
复制代码

怎么样,是不是很简单?它干的事就是判断了一下 action 的类型,如果是函数就调用,不是函数就用 dispatch 来调用,很简单。

但是它实现的功能很实用,允许我们传递函数作为 store.dispatch 的参数,这个函数的参数应该是固定的,必须符合上面代码的要求,接受 dispatch、getState作为参数,然后这个函数应该返回实际的 action。

我们也可以写一个自己的中间件了:

({ dispatch, getState }) => next => action => {
  return action.then ? action.then(next) : next(action);
}
复制代码

这个中间件允许我们传递一个 Promise 对象作为 action,然后会等 action 返回结果(一个真正的 action)之后,再进行 dispatch。

当然由于 action.then() 返回的不是实际上的 action(一个纯对象),所以这个中间件可能没法跟其他中间件一起使用,不然其他中间件接受不到 action 会出问题。这只是个示例,用于说明中间件没那么复杂,但是我们可以利用中间件做很多事情。

如果想要了解更加复杂的 redux 中间件,可以参考:

四、总结

  • Redux 精妙小巧,主要利用了闭包和观察者模式,然后运用了职责链、适配器等模式构建了一个 store 王国。store 拥有自己的领土,要想获取或改变 store 里面的内容,必须通过 store 的各个函数来实现。
  • Redux 相比于 Vuex 而言,代码量更小,定制化程度更低,这就导致易用性低于 Vuex,但是可定制性高于 Vuex。这也符合 Vue 和 React 的风格。
  • Redux 源码比较好懂,读懂源码更易于掌握 Redux 的使用方法,不要被吓倒。
  • Redux 中间件短小精悍,比较实用。如果从使用方法开始学中间件比较难懂的话,可以尝试从源码学习中间件。

最后,时间紧迫,水平有限,难免存在纰漏或错误,请大家多多包涵、多多指教、共同进步。

欢迎来我的 GitHub 下载项目源码;或者 Follow me


以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

高性能JavaScript

高性能JavaScript

【美】Nicholas C. Zakas(尼古拉斯.泽卡斯) / 丁琛 / 电子工业出版社 / 2015-8-1 / 65

如果你使用 JavaScript 构建交互丰富的 Web 应用,那么 JavaScript 代码可能是造成你的Web应用速度变慢的主要原因。《高性能JavaScript》揭示的技术和策略能帮助你在开发过程中消除性能瓶颈。你将会了解如何提升各方面的性能,包括代码的加载、运行、DOM 交互、页面生存周期等。雅虎的前端工程师 Nicholas C. Zakas 和其他五位 JavaScript 专家介绍......一起来看看 《高性能JavaScript》 这本书的介绍吧!

JSON 在线解析
JSON 在线解析

在线 JSON 格式化工具

XML、JSON 在线转换
XML、JSON 在线转换

在线XML、JSON转换工具

RGB CMYK 转换工具
RGB CMYK 转换工具

RGB CMYK 互转工具