Redux 源码之 createStore.js 与小细节分析

Redux 是试图让 state 的变化变得可预测,梳理复杂状态的 JavaScript 状态容器。遵循数据单向流动,严格的单向数据流是 Redux 架构的设计核心。Redux 的源码分析文章非常多,但是往往只是解释了每句代码做了什么,少有解释为什么这样做的文章。本文记录了笔者对 Redux 的核心文件 createStore.js 的分析与思考。

分析 Redux 源码 除了对 Redux 理解更加深入以外,重要的是回头使用起来更加得心应手。

Redux 本身很简单,小细节也非常适合笔者这样的前端小白研究学习。在文章的源码解析过程中笔者会穿插一些代码风格、逻辑处理方式等小菜鸡的代码思考环节。

下文对 Redux 具体概念请参阅 Redux 中文官方文档

createStore.js

export default function createStore(reducer, preloadedState, enhancer) {
  ...
  let currentReducer = reducer
  let currentState = preloadedState
  let currentListeners = []
  let nextListeners = currentListeners
  let isDispatching = false
  function ensureCanMutateNextListeners() { ... }

  function getState() { ... }
  function subscribe(listener) { ... }
  function dispatch(action) { ... }
  function replaceReducer(nextReducer) { ... }
  function observable() { ... }
  ...

  dispatch({ type: ActionTypes.INIT })

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

createStore() 是整个 createStore.js 中最外层的方法,功能是创建一个 Redux store 来以存放应用中所有的 state。应用中应有且仅有一个 store。上述代码简单罗列主要内容,subscribedispatchgetState 等全部都在其内部实现。下文为了简洁说明会将代码拆分,之后分析代码全部包含在 createStore() 中,配合文件源码研究更加清晰。

export default function createStore(reducer, preloadedState, enhancer) {
  // 判断 preloadedState 是一个函数,并且 enhancer 是未定义的,将 preloadedState 赋值给 enhancer 后置为 undefined
  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.')
    }
    // 如果 enhancer 符合条件,得到一个 enhancer 后的 `createStore`
    return enhancer(createStore)(reducer, preloadedState)
  }

  ...
}

enhancer 是由 applyMiddleware 函数输出。源码注释中分割线上面的代码表示,存在中间件的情况下,改造 dispatch,融入中间件,用于处理异步处理等操作。

export default function createStore(reducer, preloadedState, enhancer) {
  ...

  // reducer 必须是函数
  if (typeof reducer !== 'function') {
    throw new Error('Expected the reducer to be a function.')
  }

  // 获取 reducer - (state, action) => state
  let currentReducer = reducer

  // 拿到当前 State
  let currentState = preloadedState

  // 初始化 listeners 用于放置监听函数,用于保存快照供当前 dispatch 使用
  let currentListeners = []

  // 指向当前 listeners,在需要修改时复制出来修改为下次快照存储数据,不影响当前订阅
  let nextListeners = currentListeners
  
  // 用于标记是否正在进行 dispatch,用于控制 dispatch 依次调用不冲突
  let isDispatching = false

  // 写时复制,确保可以改变 nextListeners。没有新的监听可以始终用同一个引用
  function ensureCanMutateNextListeners() {
    // 需要写入新的监听,之前没有复制出来过的话就复制出来
    if (nextListeners === currentListeners) {
      nextListeners = currentListeners.slice()
    }
  }

  ...
}

currentListeners 是订阅器在每次 dispatch() 调用之前保存的一份快照。当你新增或取消订阅 listener 时,新的 listener 将修改内容复制到 nextListeners里,对当前的 dispatch() 不会有任何影响。对于下一次的 dispatch(),无论嵌套与否,都会使用订阅列表里最近的一次快照。createStore.js 中仅有的两个调用 ensureCanMutateNextListeners() 位置就是在 subscribe 方法与 unsubscribe 函数块中。

ensureCanMutateNextListeners 函数笔者理解为写时复制,如果在程序的生命周期中调用了 n 次 dispatch() 就为同一个 Listeners 复制 n 次,这样的性能显然不能容忍。写时复制对性能来说是一步非常好的优化,在一段时间内始终没有新的订阅或取消订阅的情况下,nextListeners 与 currentListeners 可以共用内存。如果在下次 dispatch 之前新加了监听,此时调用此函数再进行复制,不影响修改未来订阅。

getState

export default function createStore(reducer, preloadedState, enhancer) {
  ...
  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
  }
  ...
}

内容是从 Store 中读取当前稳定的状态树 State。它与 store 的最后一个 reducer 返回值相同。

String 的换行能看出作者每行最大字符接受范围的清晰代码风格。

subscribe

export default function createStore(reducer, preloadedState, enhancer) {
  ...
  function subscribe(listener) {
    // 检测新订阅的内容是否是函数
    if (typeof listener !== 'function') {
      throw new Error('Expected the listener to be a function.')
    }

    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.'
      )
    }

    // 订阅的 Bool 标记
    let isSubscribed = true

    // 确保拿到一份 nextListeners 不影响当前订阅的 Listeners
    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.'
        )
      }
      // 取消订阅,将标志位置为 false
      isSubscribed = false

      // 确保拿到一份 nextListeners 不影响当前订阅的 Listeners
      ensureCanMutateNextListeners()
      // 找到需要取消订阅的 listener index
      const index = nextListeners.indexOf(listener)
      // 移除
      nextListeners.splice(index, 1)
    }
  }
  ...
}

添加一个变化监听器。每当 dispatch action 的时候就会执行,state 树中的一部分可能已经变化。

为什么不把 subscribe 和 unsubscribe 用两个独立的代码实现?

如果订阅与取消订阅是两个独立的函数,使用者难免会不小心移除不存在的监听内容,即便代码是安全的也显得混乱。将 unsubscribe 作为返回值可以按需获取,不需要移除监听的可以不取值,需要移除监听也可以一一对应,代码层级清晰明确。

dispatch

export default function createStore(reducer, preloadedState, enhancer) {
  ...
  function dispatch(action) {
    // 标准情况下 action 必须是对象,
    // 也可以自定义中间件来传递异步操作
    if (!isPlainObject(action)) {
      throw new Error(
        'Actions must be plain objects. ' +
          'Use custom middleware for async actions.'
      )
    }

    // action 的 type 不能未定义
    if (typeof action.type === 'undefined') {
      throw new Error(
        'Actions may not have an undefined "type" property. ' +
          'Have you misspelled a constant?'
      )
    }

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

    try {
      // dispatch 正在进行,isDispatching 置为 true
      isDispatching = true
      // 旧 state 传入 action 执行 reducer,得到新 state
      currentState = currentReducer(currentState, action)
    } finally {
      // dispatch 结束,isDispatching 置为 false
      isDispatching = false
    }

    // 更新最新的监听对象,相当于:
    //    currentListeners = nextListeners
    //    const listeners = currentListeners
    const listeners = (currentListeners = nextListeners)
    // 遍历所有监听并执行
    for (let i = 0; i < listeners.length; i++) {
      const listener = listeners[i]
      listener()
    }

    return action
  }
  ...
}

const listeners = (currentListeners = nextListeners) 这段赋值对应之前说到的:

currentListeners 是订阅器在每次 dispatch() 调用之前保存的一份快照。当你新增或取消订阅 listener 时,新的 listener 将修改内容复制到 nextListeners里,对当前的 dispatch() 不会有任何影响。对于下一次的 dispatch(),无论嵌套与否,都会使用订阅列表里最近的一次快照。

dispatch() 的源码中看,此时 listeners 是这个 dispatch 的 currentListeners,在遍历期间有新的 subscribe/unsubscribe 操作都会存到当前的 nextListeners 中,不影响 listeners 的遍历。

什么时候会出现这样不安全的情况呢?

subscribe() 传入的参数 listener 就是一个函数,如果在函数里订阅或取消订阅,遍历过程中自然会出现 listeners 数组被改变的情况。currentListenersnextListenersensureCanMutateNextListeners 就是针对这种情况设计的。

replaceReducer

export default function createStore(reducer, preloadedState, enhancer) {
  ...
  function replaceReducer(nextReducer) {
    if (typeof nextReducer !== 'function') {
      throw new Error('Expected the nextReducer to be a function.')
    }

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

replaceReducer() 的功能是替换 reducer 以及初始化 Store。官方表示:

这是一个高级 API。只有在你需要实现代码分隔,而且需要立即加载一些 reducer 的时候才可能会用到它。在实现 Redux 热加载机制的时候也可能会用到。

observable

export default function createStore(reducer, preloadedState, enhancer) {
  ...
  function observable() {
    // 拿到订阅方法的函数
    const outerSubscribe = subscribe
    return {
      // 一个最小可观察订阅方法
      subscribe(observer) {
        // 判断 observer 是一个对象
        if (typeof observer !== 'object' || observer === null) {
          throw new TypeError('Expected the observer to be an object.')
        }
        // 观察者对象应该有一个 'next' 方法
        // 观察者状态改变则获取当前 state 并调用 next 方法
        function observeState() {
          if (observer.next) {
            observer.next(getState())
          }
        }

        observeState()
        // 取消订阅的方法
        const unsubscribe = outerSubscribe(observeState)
        return { unsubscribe }
      },

      // 返回为对象的私有属性,一般不暴露给开发者使用
      [$$observable]() {
        return this
      }
    }
  }
  ...
}

Redux 内部没有用到这个方法,在测试代码 redux/test/createStore.spec.js 中有出现。

ECMAScript Observable 是响应式编程的一个思想与实现提议,更多信息戳 -> proposal- obserservable

结语

看源码可以从中学到设计思路,了解优秀的框架是如何实现。除此之外,赏心悦目的 API 设计、编码顾全大局的取舍等细节也非常值得学习。细细揣摩作者的设计意图也是很有趣的。

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

推荐阅读更多精彩内容

  • 一、什么情况需要redux? 1、用户的使用方式复杂 2、不同身份的用户有不同的使用方式(比如普通用户和管...
    初晨的笔记阅读 2,011评论 0 11
  • Redux是React核心开发人员的开发的一个JavaScript 状态容器,提供可预测化的状态管理。 Redux...
    人失格阅读 840评论 0 0
  • 前面写了《React组件性能优化》《Redux性能优化》《React-Redux性能优化》,但是都没有从这些框架的...
    hepeguo阅读 731评论 0 2
  • 使用redux+react已有一段时间,刚开始使用并未深入了解其源码,最近静下心细读源码,感触颇深~ 本文主要包含...
    字节跳动技术团队阅读 1,455评论 0 5
  • export const ActionTypes = {INIT:'@@redux/INIT'} // 生成一个s...
    jiandan5850阅读 473评论 0 0