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 设计、编码顾全大局的取舍等细节也非常值得学习。细细揣摩作者的设计意图也是很有趣的。

你可能感兴趣的:(Redux 源码之 createStore.js 与小细节分析)