redux源码阅读总结(一)- createStore.js详细解析与思考

redux数据流分析

在阅读redux源码之前,先整理一下redux的数据流,官网的数据流程图如下所示。该图十分清晰明了的展示了redux的数据流:

  1. 点击UI,发起一个存钱的点击事件。
  2. 在点击事件处理函数中,dispatch分发一个action到reducer中。
  3. reducer接收当前的state和dispatch发起的action,经过计算得到一个新的state。
  4. state状态被重新更新到UI上。
    这样就形成了一个完整的数据闭环。当然还有一些小的细节,例如:很多小的reducer会被包含在一个root reducer中;reducer存在于store中;state更新UI的方式是通过subscribe里的监听函数等。

虽然图像很简单,但是绘制这样的图需要较深入的流程理解,值的好好看一下。参考链接: https://redux.js.org/tutorials/fundamentals/part-2-concepts-data-flow
在这里插入图片描述

redux文件结构分析

在此基础上,在github redux 4.x 分支上,可以看到redux的相关代码。main分支代码已被全部更新为ts,介于我对ts掌握程度不够,所以阅读的是4.x分支的代码,该分支代码仍使用的js编写方式。

源码结构如下图所示,主要文件只有六个:

applyMiddleware.js - 用于中间件应用,也是生成createStore.js文件中enhancer的方法

bindActionCreators.js - 将dispatch和action绑定

combineReducers.js - 将reducer进行combine

compose.js - 封装了reduce函数,该文件用于applyMiddleware.js中逻辑的书写

createStore.js - store相关的一些主要逻辑

index.js - 导出文件

在这里插入图片描述

createStore.js代码结构分析

这里我重要阅读和梳理一个createStore.js的源码,剩余的代码下次再分析。之所以先梳理createStore.js,是因为它是相对比较重要的一个文件,文件内的一些函数的定义刚好可以对应上面展示的数据流程图。

首先还是从宏观上观察一下createStore.js内的结构,如下图所示:

ensureCanMutateNextListeners() - 将currentListeners数组浅复制给nextListeners

getState() - 获取当前的state

subscribe(listener) - 订阅函数,用于接收监听函数

dispatch(action) - 分发action

replaceReducer(nextReducer) - 更换reducer

observable() - 非相关业务代码,不重要,想研究可以根据官方注释跳转到该链接研究https://github.com/tc39/proposal-observable

在这里插入图片描述

createStore.js代码内容分析

宏观上对文件结构和代码结构进行分析之后,有个整体的概念,再深入阅读代码,会使代码阅读的逻辑更为清晰。

下面我通过在源码中添加中文注释的方式对代码进行解释和分析。另外源代码中的英文注释也写的很清楚,对于英文注释说明的内容我就不额外翻译了。

import $$observable from './utils/symbol-observable'

import ActionTypes from './utils/actionTypes'
import isPlainObject from './utils/isPlainObject'
import { kindOf } from './utils/kindOf'

/**
 * @deprecated
 *
 * **We recommend using the `configureStore` method
 * of the `@reduxjs/toolkit` package**, which replaces `createStore`.
 *
 * Redux Toolkit is our recommended approach for writing Redux logic today,
 * including store setup, reducers, data fetching, and more.
 *
 * **For more details, please read this Redux docs page:**
 * **https://redux.js.org/introduction/why-rtk-is-redux-today**
 *
 * `configureStore` from Redux Toolkit is an improved version of `createStore` that
 * simplifies setup and helps avoid common bugs.
 *
 * You should not be using the `redux` core package by itself today, except for learning purposes.
 * The `createStore` method from the core `redux` package will not be removed, but we encourage
 * all users to migrate to using Redux Toolkit for all Redux code.
 *
 * If you want to use `createStore` without this visual deprecation warning, use
 * the `legacy_createStore` import instead:
 *
 * `import { legacy_createStore as createStore} from 'redux'`
 *
 */
// 开头的文档注释反复强调了不推荐直接使用createStore,推荐使用@reduxjs/toolkit包
// 如果非要用createStore,也建议使用import { legacy_createStore as createStore} from 'redux'的方式
export function createStore(reducer, preloadedState, enhancer) {
  // 只能传入一个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. See https://redux.js.org/tutorials/fundamentals/part-4-store#creating-a-store-with-enhancers for an example.'
    )
  }

  // 只传reducer和一个函数的情况下,该函数是enhancer
  if (typeof preloadedState === 'function' && typeof enhancer === 'undefined') {
    enhancer = preloadedState
    preloadedState = undefined
  }

  // 传入的enhancer只能是函数
  if (typeof enhancer !== 'undefined') {
    if (typeof enhancer !== 'function') {
      throw new Error(
        `Expected the enhancer to be a function. Instead, received: '${kindOf(
          enhancer
        )}'`
      )
    }

    return enhancer(createStore)(reducer, preloadedState)
  }

  // reducer必须是函数
  if (typeof reducer !== 'function') {
    throw new Error(
      `Expected the root reducer to be a function. Instead, received: '${kindOf(
        reducer
      )}'`
    )
  }

  // 变量顾名思义都很容易理解
  let currentReducer = reducer
  let currentState = preloadedState
  let currentListeners = []
  let nextListeners = currentListeners	// 思考点:为什么定义两个监听数组?(后文和注释中有解释)
  let isDispatching = false

  /**
   * This makes a shallow copy of currentListeners so we can use
   * nextListeners as a temporary list while dispatching.
   *
   * This prevents any bugs around consumers calling
   * subscribe/unsubscribe in the middle of a dispatch.
   */
  // 这段代码很好理解,就是一个简单的复制过程
  // 思考点:为什么要nextListeners === currentListeners判断后才赋值?(后文有解释)
  function ensureCanMutateNextListeners() {
    if (nextListeners === currentListeners) {
      nextListeners = currentListeners.slice()
    }
  }

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

    return currentState
  }

  /**
   * Adds a change listener. It will be called any time an action is dispatched,
   * and some part of the state tree may potentially have changed. You may then
   * call `getState()` to read the current state tree inside the callback.
   *
   * You may call `dispatch()` from a change listener, with the following
   * caveats:
   *
   * 1. The subscriptions are snapshotted just before every `dispatch()` call.
   * If you subscribe or unsubscribe while the listeners are being invoked, this
   * will not have any effect on the `dispatch()` that is currently in progress.
   * However, the next `dispatch()` call, whether nested or not, will use a more
   * recent snapshot of the subscription list.
   *
   * 2. The listener should not expect to see all state changes, as the state
   * might have been updated multiple times during a nested `dispatch()` before
   * the listener is called. It is, however, guaranteed that all subscribers
   * registered before the `dispatch()` started will be called with the latest
   * state by the time it exits.
   *
   * @param {Function} listener A callback to be invoked on every dispatch.
   * @returns {Function} A function to remove this change listener.
   */
  // 上面那段英文注释真的写的特别的好,建议仔细阅读,它告诉我们在dispatch时,我们应用的是当前的subscribe快照
  // 如果调用dispatch期间改变了listener数组,其实是不会影响当前使用的快照的,而是应用到下一次dispatch中,因为下一次dispatch之前会产生一个新的快照,这个新快照就会包含改变的数组
  // 这其实也是为什么应用两个listener数组的原因,即保持当前dispatch进行中时遍历的listener数组的不变性
  function subscribe(listener) {
    if (typeof listener !== 'function') {
      throw new Error(
        `Expected the listener to be a function. Instead, received: '${kindOf(
          listener
        )}'`
      )
    }

    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/store#subscribelistener for more details.'
      )
    }

    let isSubscribed = true

    // 增加监听函数
    ensureCanMutateNextListeners()
    nextListeners.push(listener)

    // 返回解除监听函数
    return function unsubscribe() {
      // 防止多次解除订阅
      if (!isSubscribed) {
        return
      }

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

      isSubscribed = false

      // 解除当前监听的函数
      ensureCanMutateNextListeners()
      const index = nextListeners.indexOf(listener)
      nextListeners.splice(index, 1)
      // 思考点:这里数组为什么置null?(后文有解释)
      currentListeners = null
    }
  }

  /**
   * Dispatches an action. It is the only way to trigger a state change.
   *
   * The `reducer` function, used to create the store, will be called with the
   * current state tree and the given `action`. Its return value will
   * be considered the **next** state of the tree, and the change listeners
   * will be notified.
   *
   * The base implementation only supports plain object actions. If you want to
   * dispatch a Promise, an Observable, a thunk, or something else, you need to
   * wrap your store creating function into the corresponding middleware. For
   * example, see the documentation for the `redux-thunk` package. Even the
   * middleware will eventually dispatch plain object actions using this method.
   *
   * @param {Object} action A plain object representing “what changed”. It is
   * a good idea to keep actions serializable so you can record and replay user
   * sessions, or use the time travelling `redux-devtools`. An action must have
   * a `type` property which may not be `undefined`. It is a good idea to use
   * string constants for action types.
   *
   * @returns {Object} For convenience, the same action object you dispatched.
   *
   * Note that, if you use a custom middleware, it may wrap `dispatch()` to
   * return something else (for example, a Promise you can await).
   */
  function dispatch(action) {
    // 限制了action的类型
    if (!isPlainObject(action)) {
      throw new Error(
        `Actions must be plain objects. Instead, the actual type was: '${kindOf(
          action
        )}'. You may need to add middleware to your store setup to handle dispatching other values, such as 'redux-thunk' to handle dispatching functions. See https://redux.js.org/tutorials/fundamentals/part-4-store#middleware and https://redux.js.org/tutorials/fundamentals/part-6-async-logic#using-the-redux-thunk-middleware for examples.`
      )
    }

    // action必须要有type
    if (typeof action.type === 'undefined') {
      throw new Error(
        'Actions may not have an undefined "type" property. You may have misspelled an action type string constant.'
      )
    }

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

	// 触发reducer
    try {
      isDispatching = true
      currentState = currentReducer(currentState, action)
    } finally {
      isDispatching = false
    }

	// subscribe时我们操作的都是nextListeners,因此要先把最新的nextListeners赋值给currentListeners,然后再遍历,这就是获取最新快照的过程
	// 由于我们遍历的是currentListeners,而subscribe时操作的是nextListeners,就这保证了dispatch时当前遍历数组的不变性,例如在遍历过程中如果又触发了额外的subscribe操作或者unsubscribe操作,也只会改变nextListeners,而这种改变不会影响当前的监听函数调用过程,只会影响下次dispatch时监听函数调用过程
    // 使用for循环遍历性能会远高于forEach
    const listeners = (currentListeners = nextListeners)
    for (let i = 0; i < listeners.length; i++) {
      const listener = listeners[i]
      listener()
    }

    return action
  }

  /**
   * Replaces the reducer currently used by the store to calculate the state.
   *
   * You might need this if your app implements code splitting and you want to
   * load some of the reducers dynamically. You might also need this if you
   * implement a hot reloading mechanism for Redux.
   *
   * @param {Function} nextReducer The reducer for the store to use instead.
   * @returns {void}
   */
  // 简单易懂的代码没什么好说的
  function replaceReducer(nextReducer) {
    if (typeof nextReducer !== 'function') {
      throw new Error(
        `Expected the nextReducer to be a function. Instead, received: '${kindOf(
          nextReducer
        )}`
      )
    }

    currentReducer = nextReducer

    // This action has a similiar effect to ActionTypes.INIT.
    // Any reducers that existed in both the new and old rootReducer
    // will receive the previous state. This effectively populates
    // the new state tree with any relevant data from the old one.
    dispatch({ type: ActionTypes.REPLACE })
  }

  /**
   * Interoperability point for observable/reactive libraries.
   * @returns {observable} A minimal observable of state changes.
   * For more information, see the observable proposal:
   * https://github.com/tc39/proposal-observable
   */
  // 非主业务代码,感兴趣的去看注释里的github链接,这里不深入研究
  function observable() {
    const outerSubscribe = subscribe
    return {
      /**
       * The minimal observable subscription method.
       * @param {Object} observer Any object that can be used as an observer.
       * The observer object should have a `next` method.
       * @returns {subscription} An object with an `unsubscribe` method that can
       * be used to unsubscribe the observable from the store, and prevent further
       * emission of values from the observable.
       */
      subscribe(observer) {
        if (typeof observer !== 'object' || observer === null) {
          throw new TypeError(
            `Expected the observer to be an object. Instead, received: '${kindOf(
              observer
            )}'`
          )
        }

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

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

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

  // When a store is created, an "INIT" action is dispatched so that every
  // reducer returns their initial state. This effectively populates
  // the initial state tree.
  // 初始化一个state树,否则的话拿到的是一个undefine
  dispatch({ type: ActionTypes.INIT })

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

/**
 * Creates a Redux store that holds the state tree.
 *
 * **We recommend using `configureStore` from the
 * `@reduxjs/toolkit` package**, which replaces `createStore`:
 * **https://redux.js.org/introduction/why-rtk-is-redux-today**
 *
 * The only way to change the data in the store is to call `dispatch()` on it.
 *
 * There should only be a single store in your app. To specify how different
 * parts of the state tree respond to actions, you may combine several reducers
 * into a single reducer function by using `combineReducers`.
 *
 * @param {Function} reducer A function that returns the next state tree, given
 * the current state tree and the action to handle.
 *
 * @param {any} [preloadedState] The initial state. You may optionally specify it
 * to hydrate the state from the server in universal apps, or to restore a
 * previously serialized user session.
 * If you use `combineReducers` to produce the root reducer function, this must be
 * an object with the same shape as `combineReducers` keys.
 *
 * @param {Function} [enhancer] The store enhancer. You may optionally specify it
 * to enhance the store with third-party capabilities such as middleware,
 * time travel, persistence, etc. The only store enhancer that ships with Redux
 * is `applyMiddleware()`.
 *
 * @returns {Store} A Redux store that lets you read the state, dispatch actions
 * and subscribe to changes.
 */
// 这里的export呼应了文档开头推荐使用import { legacy_createStore as createStore} from 'redux'引入模块
export const legacy_createStore = createStore

在注释里我对代码进行了详细清晰的讲解,如有错误或者不理解的部分可以联系我一起探讨。

下面是几个我对代码的思考:

  1. gitState拿到的是什么?能否直接修改state?
  2. 为什么用到了两个listener数组?
  3. 为什么ensureCanMutateNextListeners函数中需要先进行nextListeners === currentListeners判断,然后才赋值nextListeners = currentListeners.slice()?
  4. 为什么dispatch触发reducer时不允许getState(),不允许subscribe(listener),不允许unsubscribe(),不允许dispatch(action)?
  5. 为什么unsubscribe时,currentListeners数组置null?

以下是我对上面几个问题思考的一些结果:

  1. getState可以直接拿到currentState,因此理论上来讲其实可以直接修改当前的state,但是如果直接修改了该值,就没有后续操作了。而redux之所以希望我们使用dispatch去修改state,是因为使用dispatch修改state之后,会依次调用订阅的listener函数,这样就能实时响应识别state的改变。
  2. 对于该问题我在注释中也进行了较详细的阐述。结合subscribe()和dispatch()注释中我的分析,总的来说就是保证在dispatch时,只会触发当前dispatch之前我们订阅的listener,而在dispatch过程中改变的监听数组会应用在下一次的dispatch中,保证当前监听数组的不变性,得到预期的结果。
  3. 该问题和上一个问题是相关的,因为ensureCanMutateNextListeners只在subscribe函数和unsubscribe函数中使用了,之所以要先判断相等才赋值是因为如果dispatch遍历currentListeners过程中又额外触发了subscribe或者unsubscribe操作,那么此时如果直接赋值就会导致更新的nextListeners又恢复到和原来的currentListeners一样了。举例说明:例如在dispatch过程中,遍历currentListeners时,又额外触发了两次subscribe函数,那么第一次进入subscribe时,在往nextListeners里push listener之前,调用ensureCanMutateNextListener,此时nextListeners和currentListeners是一样的,此时判不判断相等都不影响。而当我们把新的listener push到nextListeners之后,此时nextListeners就和currentListeners不同了,相当于比currentListeners多了一个listener。那么在这次subscribe结束后,第二次进入subscribe时,依旧是在往nextListeners里push listener之前,调用ensureCanMutateNextListeners函数时,此时两个数组不相等,如果在这种情况下进行赋值,就会将nextListeners恢复为第一次进入subscribe之前的状态。导致的结果就是,我们预期获得多两个listener的nextListeners数组,而没有了相等才赋值的判断之后,我们获得了只增加了最后一个listener的nextListeners数组,丢了一个想添加的listener。而unsubscribe甚至有currentListeners=null的操作,如果多次unsubscribe还强行赋值,nextListeners也跟着变成null了。
  4. reducer会造成currentState的改变,而getState()、subscribe(listener)、unsubscribe()和dispatch(action)会涉及到对状态的改变或者识别,我认为强行操作会导致结果无法预期。
  5. 这里之所以会有这个问题,是因为在unsubscribe的时候,redux会删除nextListeners数组中想要解除订阅的listener,而在dispatch时,会有种这样一个赋值操作currentListeners = nextListeners,全程currentListeners的改变都只来源于nextListeners的赋值,那么为什么还要多此一举在unsubscribe时将currentListeners=null呢?这里涉及到js的gc问题,由redux代码可知,currentListeners = nextListeners,即currentListeners永远来自nextListeners,而只有在调用subscribe或者unsubscribe,且满足nextListeners === currentListeners时,才有nextListeners = currentListeners.slice()。unsubscribe时执行currentListeners=null这会导致一种情况,就是当我们调用一个unsubscribe的后,就会产生nextListeners !== currentListeners的情况,而这种情况下,将绝对不会再用到currentListeners的值!无论之后我们是订阅、解除订阅还是执行dispatch(action),都不会再用到currentListeners的值,那么这种情况下对于currentListeners指向的那些存在堆里的值就完全没必要存储了,及时置null相当于手动让其进行垃圾回收。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值