深入 Redux 之 createStore

前言

Redux 本身指 redux 这个 npm 包,它提供如干 API 让我们使用 reducer 创建 store,并能够更新 store 中的数据或获取 store 中最新状态。
Redux适用的场景:多交互、多数据源,当应用不复杂的时候,就没必要使用它。

Redux三大原则

1. 单一数据源

在 Redux 的思想里,一个应用永远只有唯一的数据源。
使用单一数据源的好处在于整个应用状态都保存在一个对象中,这样我们随时可以提取出整个应用的状态进行持久化。

2. 状态是只读的

定义一个 reducer,它的功能是根据当前触发的action对当前应用的状态进行迭代,这里并没有直接修改应用的状态,而是返回了一份全新的状态。
Redux 提供的 createStore 方法会根据 reducer 生成 store,可以利用store.dispatch 方法来达到修改状态的目的。

3. 状态修改均由纯函数完成

在Redux里,我们通过定义 reducer 来确定状态的修改,而每一个 reducer都是纯函数,这意味着它没有副作用,即接受一定的输入,必定会得到一定的输出。

Redux 核心 API

Redux 的核心是一个 store,这个 store,这个 store 由 Redux 提供的 createStore(reducers[,initialState]) 方法生成。要想生成 store,reducers,同时也可以传入第二个可选参数初始化状态 initialState。

在 Redux 里,负责相应 action 并修改数据的角色就是 reducer。reducer本质上是一个函数,其函数签名为 reducer(previousState, action) => newState。可以看出,reducer 在处理 action 的同时,还需要接受一个 previousState 参数。所以reducert的职责就是根据 previousState 和 action 计算出新的 newState。

在实际应用中,reducer 在处理 previousState 时,还需要有一个特殊的非空判断。很显然,reducer第一次执行的时候,并没有任何的 previousState,而 reducer 的最终职责是返回新的 state,因此需要在这种特殊情况下定义好的 initialState。

Redux 最核心的 API ---- createStore,通过 createStore 方法创建的 store 是一个对象,它本身又包含4个方法。

  • getState():获得store中当前的状态。
  • dispath(action):分发一个action,并返回这个 action,这是唯一能改变 store 中数据的方式。
  • subscrible(listener): 注册一个监听者,他在 store 发生变化时被调用。
  • replaceReducer(nextReducer):更新当前 store 里的reducer,一般只会在开发模式中调用该方法。

在实际应用中,我门最常用的是 getState() 和 dispatch() 这两个方法。至于subscribe() 和 replaceReducer() 方法,一般会在 Redux 与某个系统(如 React ) 做桥接的时候使用。

Redux 源码之 createStore

解读 redux 源码之 createStore,代码目录在 redux/src/createStore。
对应 redux 版本:3.7.2。

import isPlainObject from 'lodash/isPlainObject'
import $$observable from 'symbol-observable'

/**
 * 这是 redux 保留的私有的 action types。
 * 对于任何未知的 actions,你必须要返回当前的状态。
 * 如果当前的状态是没有定义的,你都要返回一个初始的状态。
 * 不要在你的代码中直接引用这些 action types。
 */
export const ActionTypes = {
  INIT: '@@redux/INIT'
}

/**
 * 创建一个持有状态树的 redux store。
 * 调用dispatch() 是唯一的一种方式去修改 store中的的值。
 * 应用中应该只有一个 store。为了将程序状态中不同部分的变更逻辑
 * 组合在一起,你需要使用 combineReducers 将一些
 * reducers 合并成一个reducer
 *
 * @param {Function} reducer 一个返回下一个状态树的方法,需要提供当
 *  前的状态树和要发送的 action。
 *
 * @param {any} [preloadedState] 初始的状态。
 * 您可以选择指定它来保存通用应用程序中服务器的状态,或者恢复
 * 以前序列化的用户会话。
 * 如果你使用了`combineReducers`方法来生成最终的reducer。那么这个初始状
 * 态对象的结构必须与调用`combineReducers`方法时传入的参数的结构保持相
 * 同。
 *
 * @param {Function} [enhancer] store增强器。你可以选择性的传入一个增强函
 * 数取增强 store,例如中间件,时间旅行,持久化。这 redux 唯一一个自带的
 * 增强器是的 applyMiddleware
 *
 * @returns {Store} 一个可以让你读状态,发布 actions 和订阅变化的 redux 
 * store
 */
export default function createStore(reducer, preloadedState, enhancer) {
  // 如果 preloadedState类型是function,enhancer类型是undefined,那认为用
  // 户没有传入preloadedState,就将preloadedState的值传给  
  // enhancer,preloadedState值设置为undefined
  if (typeof preloadedState === 'function' && typeof enhancer === 'undefined') {
    enhancer = preloadedState
    preloadedState = undefined
  }
  // enhancer类型必须是一个function
  if (typeof enhancer !== 'undefined') {
    if (typeof enhancer !== 'function') {
      throw new Error('Expected the enhancer to be a function.')
    }
    // 返回使用enhancer增强后的store
    return enhancer(createStore)(reducer, preloadedState)
  }
  // reducer必须是一个function
  if (typeof reducer !== 'function') {
    throw new Error('Expected the reducer to be a function.')
  }

  let currentReducer = reducer
  let currentState = preloadedState
  let currentListeners = []
  let nextListeners = currentListeners
  let isDispatching = false

  // 在每次修改监听函数数组之前复制一份,实际的修改的是新
  // 复制出来的数组上。确保在某次 dispatch 发生前就存在的监听器,
  // 在该次dispatch之后都能被触发一次。
  function ensureCanMutateNextListeners() {
    if (nextListeners === currentListeners) {
      nextListeners = currentListeners.slice()
    }
  }

  /**
   * 读取 store 管理的状态树
   *
   * @returns {any} 返回应用中当前的状态树
   */
  function getState() {
    return currentState
  }

  /**
   * 添加一个改变监听器。它将在一个action被分发的时候触发,并且状态数的某
   * 些部分可能已经发生了变化。那么你可以调用 getState 来读取回调中的当前
   * 状态树。
   *    
   * 你可以从一个改变的监听中调用 dispatch(),注意事项:
   *
   * 1.在每一次调用 dispatch() 之前监听器数组都会被复制一份。如果你在监听函
   * 数中订阅或者取消订阅,这个不会影响当前正在进行的 dispatch()。而下次
  * dispatch()是否是嵌套调用,都会使用最新的修改后的监听列表。
   * 2.监听器不希望看到哦啊所有状态的改变,如状态可能在监听器被调用前可能
   * 在嵌套 dispatch() 可能更新过多次。但是,在某次dispatch
   * 触发之前已经注册的监听函数都可以读取到这次diapatch之后store的最新状
   * 态。
   *
   * @param {Function} listener 在每次 dispatch 之后会执行的回调函数。
   * @returns {Function} 返回一个用于取消这次订阅的函数。
   */
  function subscribe(listener) {
    if (typeof listener !== 'function') {
      throw new Error('Expected listener to be a function.')
    }

    let isSubscribed = true

    ensureCanMutateNextListeners()
    nextListeners.push(listener)

    return function unsubscribe() {
      if (!isSubscribed) {
        return
      }

      isSubscribed = false

      ensureCanMutateNextListeners()
      const index = nextListeners.indexOf(listener)
      nextListeners.splice(index, 1)
    }
  }

  /**
   * 发送一个 action,这是唯一一种触发状态改变的方法。
   * 每次发送 action,用于创建 store 的 `reducer` 都会被调用一次。调用时传入
   * 的参数是当前的状态以及被发送的 action。的返回值将被当作下一次的状
   * 态,并且监听器将会被通知。
   *
   * 基础实现只支持简单对象的 actions。如果你希望可以发送 
   * Promise,Observable,thunk火气其他形式的 action,你需要用相应的中间
   * 件把 store创建函数封装起来 。例如,你可以参阅 `redux-thunk`包的文档。
   * 不过这些中间件还是通过 dispatch 方法发送简单对象形式的 action。
   * 
   * @param {Object} action,一个标识改变了什么的对象。这是一个很好的点子
   * 保证 actions 可被序列化,这样你就可以记录并且回放用户的操作,或者使用
   * 可以穿梭时间的插件 `redux-devtools`。一个 action 必须有一个值不为
   * `undefined`的type属性,推荐使用字符串常量作为 action types。
   *
   * @returns {Object} 为了方便起见,返回传入的 action 对象。
   *
   * 要注意的是,如果你使用一个自定义的中间件,可能会把`dispatch()`的返回
   * 值封装成其他内容(比如,一个可以await的Promise)。
   */
  function dispatch(action) {
    // 如果 action不是一个简单对象,抛出异常
    if (!isPlainObject(action)) {
      throw new Error(
        'Actions must be plain objects. ' +
        'Use custom middleware for async actions.'
      )
    }

    if (typeof action.type === 'undefined') {
      throw new Error(
        'Actions may not have an undefined "type" property. ' +
        'Have you misspelled a constant?'
      )
    }
    
    // reducer内部不允许再次调用dispatch,否则抛出异常
    if (isDispatching) {
      throw new Error('Reducers may not dispatch actions.')
    }

    try {
      isDispatching = true
      currentState = currentReducer(currentState, action)
    } finally {
      isDispatching = false
    }

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

    return action
  }

  /**
   * 替换 store 当前使用的 reducer 函数。
   *
   * 如果你的程序代码实现了代码拆分,并且你希望动态加载某些 reducers。或
   * 者你为 redux 实现一个热加载的时候,你也会用到它。
   *
   * @param {Function} nextReducer 替换后的reducer
   * @returns {void}
   */
  function replaceReducer(nextReducer) {
    if (typeof nextReducer !== 'function') {
      throw new Error('Expected the nextReducer to be a function.')
    }

    currentReducer = nextReducer
    dispatch({ type: ActionTypes.INIT })
  }

  /**
   * 为 observable/reactive库预留的交互接口。
   * @returns {observable} 标识状态变更的最简单 observable兑现。
   * 想要获得更多的信息,可以查看 observable的提案:
   * https://github.com/tc39/proposal-observable
   */
  function observable() {
    const outerSubscribe = subscribe
    return {
      /**
       * 一个最简单的 observable 订阅方法。
       * @param {Object} observer,任何的可以被作为observer使用的对象。
       * observer对象应该包含`next`方法。
       * @returns {subscription} 返回一个 object 带有用于从store 解除 observable并且进一步停止接收 值 的`unsubscribe`方法的对象。
       */
      subscribe(observer) {
        if (typeof observer !== 'object') {
          throw new TypeError('Expected the observer to be an object.')
        }

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

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

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

  // 当一个store创建好,一个 "INIT" 的 action 就会分发,以便每个 reducer返回
  // 初始的状态,这有效填充初始的状态树。
  dispatch({ type: ActionTypes.INIT })

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,997评论 6 502
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,603评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 163,359评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,309评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,346评论 6 390
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,258评论 1 300
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,122评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,970评论 0 275
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,403评论 1 313
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,596评论 3 334
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,769评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,464评论 5 344
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,075评论 3 327
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,705评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,848评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,831评论 2 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,678评论 2 354

推荐阅读更多精彩内容