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


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

查看所有标签

猜你喜欢:

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

产品故事地图

产品故事地图

唐娜·理查(Donna Lichaw) / 向振东 / 机械工业出版社 / 2017-6 / 49.9元

本书一共8章,分为三个部分:第1-2章讲述故事的作用、你该如何运用产品故事来吸引顾客,不是通过讲故事,而是创造故事。第3-5章阐述了不同情境和客户生命周期中的产品故事类型。第6-8章进一步研究如何在战略和策略层面发现、提升、用好你的产品故事。 《产品故事地图》写给那些想要通过创造出顾客喜欢用、经常用而且会推荐给别人用的产品来吸引客户的人。这里的“产品”包括网页、软件、APP、数字化或非数字化......一起来看看 《产品故事地图》 这本书的介绍吧!

SHA 加密
SHA 加密

SHA 加密工具

UNIX 时间戳转换
UNIX 时间戳转换

UNIX 时间戳转换

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

RGB CMYK 互转工具