Logo

Redux源码分析--数据中心篇

avatar lan 14 Jun 2018

在如今的前端浪潮中,React和Redux有着举足轻重的地位。React,Redux再加上用于链接他们的代码库就足以让一些没有足够经验的开发者迷失到代码的海洋里,很容易让程序员们培养成一种别人怎么写我就怎么写的编码习惯,难怪许多大神会说这是最好的时代但也是最坏的时代。

Redux

今天我想脱离整体来看局部,从源码的角度上来剖析Redux到底是个什么玩意,了解了它的原理才不至于在如今的浪潮中显得手忙脚乱。

前言

抛开React不谈,Redux其实就只是一个管理状态的数据中心,然而作为一个数据中心它的特色在于我们不能够直接修改数据中心里面的数据,我们需要自行定义操作逻辑 reducer,以及操作类型 action ,通过分发不同的 action 来匹配 reducer 里面对应的操作,才能达到修改数据的目的。

一般来说我们会通过以下方式来创建一个数据中心

import { createStore } from 'redux'
const store = createStore(...blablabla)

这里最为关键的就是createStore这个函数,接下来我想详细地对它做个分析。

createStore方法剖析

createStore.js这个文件纯代码的部分大概有100多行,如果把他们全部贴出来再一一分析并非明智之举,我认为只对关键的部分进行分析是更恰当的做法。要分析一个方法我觉得比较有意义的是看它接收了什么,以及返回了什么。

1) 接收的参数

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

这个方法接受三个参数,分别是 reducer , preloadedState , enhancer 。以上都分别可以由开发者进行定义,reducer 就是由开发者定义的一个操作方法,它会以旧的状态作为参数,处理过后返回一个新的状态。preloadedState则可以理解成数据中心的初始状态,它是个可选值。

最后的enhancer又是什么呢?从字面上理解它是一个增强器,用于增强createStore。从源码看它的工作方式

export default function createStore(reducer, preloadedState, enhancer) {
  .....
  if (typeof preloadedState === 'function' && typeof enhancer === 'undefined') { // 参数归一
    enhancer = preloadedState
    preloadedState = undefined
  }

  if (typeof enhancer !== 'undefined') {
    if (typeof enhancer !== 'function') {
      throw new Error('Expected the enhancer to be a function.')
    }

    return enhancer(createStore)(reducer, preloadedState) // 直接返回一个增强后的`createStore
  }
  .....
}

可见,它接收了原来的 createStore 作为参数,并且返回了一个增强了的方法,最后用增强过的方法来调用原来传入的参数。了解原理之后我们可以很容易地写出一个 状态打印增强器 ,用于打印 dispatch 前后的状态信息。

.....
function enhancer(createStore) {
  return (reducer, initialState, enhancer) => {
    const store = createStore(reducer, initialState, enhancer)

    function dispatch(action) {
      console.log('old', store.getState())
      const res = store.dispatch(action);
      console.log('new', store.getState())
      return res
    }

    // 用心的dispatch方法来替换原有的dispatch方法
    return {
      ...store,
      dispatch
    }
  }
}

const store = createStore(reducers, undefined, enhancer)

另外,从Redux的源码可以看到 createStore 做了一种叫做参数归一的处理,在许多JS库中都会采用这种方式兼容不同情况下的参数传入。当我们不需要传入初始状态,而只需要使用enhancer增强器的时候,我们还可以把代码写成这样

const store = createStore(reducers, enhancer)

2) 返回值

接下来我们看看返回值。createStore最终会返回一个对象,包含的东西如下

import $$observable from 'symbol-observable'

export default function createStore(reducer, preloadedState, enhancer) {
  .....
  return {
    dispatch,
    subscribe,
    getState,
    replaceReducer,
    [$$observable]: observable
  }
}

这些便是我们数据中心为外部提供的全部接口了。最后一个看起来有点奇怪,其他的从字面上应该都比较容易理解,容许许我一一分析。

a. getState–返回当前状态

Redux的核心理念之一就是不支持直接修改状态,它是通过闭包来实现这一点。

export default function createStore(reducer, preloadedState, enhancer) {
  let currentState = preloadedState

  function getState() {
    .....
    return currentState
  }
}

它先是定义了一个内部的变量 currentState ,然后通过一个名为getState的方法来返回它的值。这就造成了currentState这个状态对我们而言是只读的,我们没办法直接修改它的值。在代码里面我们可以通过getState这个方法来返回当前状态

console.log(store.getState())

b. subscribe–构造监听者队列

每个store本身会维护一个监听者队列,我们可以把它想象成一个方法的队列,在每次分发action的时候都会依次调用监听者队列中所有方法。通过这个subscribe方法可以手动地把一些回调函数添加到监听者队列中

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

  let currentListeners = []
  let nextListeners = currentListeners

  ...

  function ensureCanMutateNextListeners() {
    if (nextListeners === currentListeners) {
      nextListeners = currentListeners.slice()
    }
  }

  ...

  function subscribe(listener) {
    .....

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

}

逻辑其实很简单,为了减少篇幅我把一些类型检查的代码去掉了。每次调用subscribe的时候传入一个回调函数,subscribe 会把它放到一个监听者队列中去,并返回一个 unsubscribe 的方法。这个 unsubscribe 方法是让开发者可以方便地从列表中删除对应的回调函数,此外该方法还维护着一个 isSubscribed 标识订阅状态。

这里面有一个比较有意思的 ensureCanMutateNextListeners 的方法,按照代码的逻辑,它是要保证监听者的添加与删除并不在currentListeners这个原始的队列里面进行直接操作,我们操作的只是它的一个副本。直到我们调用dispatch方法进行分发的时候,currentListenersnextListeners才会再一次指向同一个对象,这个在后面的代码里面会看到。

c. dispatch–低调的action分发者

dispatch 方法是用来分发 action 的,可以把它理解成用于触发数据更新的方法。它的核心实现也比较简单

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

    // 调用reducer
    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
  }
}

我依旧把一些类型检查的代码去掉,首先dispatch方法会以当前的状态currentState以及我们定义的动作action作为参数来调用当前的reducer方法。另外它使用isDispatching变量来记录分发的状态,正在分发则设置为true。这里需要注意的是我们的reducer方法将会被设置成一个纯函数–它不会产生副作用,并且对于同样的输入它会返回同样的输出。换句话说它不会直接在原来状态的基础上进行修改,而是会直接返回一个新的状态,并对原有状态进行替换。

完成了上面这些之后我们会依次遍历所有的监听者,并且手动调用所有的回调函数。这里需要注意的是之前有讲过的,订阅/取消订阅的时候我们会生成一个currentListeners的副本nextListeners并在它上面添加/删除回调函数。然而到了dispatch这一步他们会做一次同步,这样他们就又会指向同一个对象了。

d. replaceReducer–替换当前的reducer

replaceReducer这个方法做的事情其实很简单,它可以用新的reducer替换掉当前的reducer,并且分发一个替换的action,下面是源代码

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

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

据说这种方式在调试环境下会用得比较多。在正式环境下一般都不会在中途更替reducer,以免得增加维护成本。

e. observable–观察者

这个是比较让我费解的一个功能了,然而Redux的数据中心居然把它作为api开放出来,咱门先贴源码

export default function createStore(reducer, preloadedState, enhancer) {
  ....
  function observable() {
    const outerSubscribe = subscribe
    return {
      ...
      subscribe(observer) {
        ....
        function observeState() {
          if (observer.next) {
            observer.next(getState())
          }
        }

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

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

如果直接调用这个接口,它会返回一个对象,而对象里面包含了subscribe方法,并且我们可以把一个包含next字段(它是一个函数)的对象作为subscribe方法的参数,就可以在每次数据变动的时候以当前状态getState()作为参数调用next所携带的函数。

这么说有点拗口,可能给个例子会比较直观

import $$observable from 'symbol-observable'

......
const store = createStore(reducer)

const subObject = store[$$observable]()
subObject.subscribe({
  next: (a) => {
    console.log(a)
  }
})

这样就可以做到每次动作被分发的时候都会调用 next 所携带的方法,并打印出 getState() 的值。这种观察者模式的写法有什么特殊的意义我也还没有时间去深究,似乎是草案的一部分,估计目前用的也不多,先不深入探究了。

尾声

这篇文章的原标题是Redux源码分析,但由于本人概括能力有限,感觉只用一篇文章要分析完整个Redux的源码有点艰难,所以最后还是决定拆分,这篇文章主要讲解Redux的数据中心store到底是什么玩意,分别对store开放的api进行源码分析,简单了解了一下它的工作原理。

Happy Coding and Writing!!

Tags
Redux
react
Javascript