vuex是vue官方出品的用来实现共享状态管理的一个插件。我之前也仿照vuex的API实现了一个低配版。实现vuex其实并不是太难,我的实现就五百行左右吧,官方实现多点但是核心也不超过一千行。实现vuex作为一个加深对vue理解的练手项目还是不错的。

vuex中的state和getters对应vue实例中的data和computed,vuex的mutations/commit、actions/dispatch本质上是事件机制,vuex的subscribe和subscribeAction更接近于钩子机制。可以说只要熟悉vue,实现vuex的前提条件就满足了。

我的实现和vuex最大的差异在于对module的理解。vuex的module更倾向于是提供一个业务代码组织方式,我的实现是把module当成一个独立单元(store实例),这些store实例按照树状结构组织起来,并且相应的属性会按照命名空间被代理到根store实例上,最终暴露出来的是根store实例。vuex只会对应一个store实例,这一个store实例会对应一棵module实例树。在vuex中ModuleCollection负责连接store实例和module实例树,但是它更接近于一个工具类。

由module引申出来的一个概念是localContext。在vuex的文档中经常会看到rootState、rootGetters这样的字眼,他们其实对应rootContext。localContext是限制在了一个module中。在我的实现中,每一个module都会对应一个store实例,localContext可以认为是每个module对应的store实例。在vuex中由于只有一个store实例,需要做一些操作获取localContext。

function makeLocalContext (store, namespace, path) {
  const noNamespace = namespace === ''

  const local = {
    // 对应dispatch方法第一个参数的dispatch
    dispatch: noNamespace ? store.dispatch : (_type, _payload, _options) => {
      const args = unifyObjectStyle(_type, _payload, _options)
      const { payload, options } = args
      let { type } = args

      if (!options || !options.root) {
        // 核心是根据命名空间获取相对于根的dispatch type参数
        type = namespace + type
        if (process.env.NODE_ENV !== 'production' && !store._actions[type]) {
          console.error(`[vuex] unknown local action type: ${args.type}, global type: ${type}`)
          return
        }
      }

      return store.dispatch(type, payload)
    },
    // 对应dispatch方法第一个参数的commit
    // 思路与dispatch一致
    commit: noNamespace ? store.commit : (_type, _payload, _options) => {
      const args = unifyObjectStyle(_type, _payload, _options)
      const { payload, options } = args
      let { type } = args

      if (!options || !options.root) {
        type = namespace + type
        if (process.env.NODE_ENV !== 'production' && !store._mutations[type]) {
          console.error(`[vuex] unknown local mutation type: ${args.type}, global type: ${type}`)
          return
        }
      }

      store.commit(type, payload, options)
    }
  }

  // getters and state object must be gotten lazily
  // because they will be changed by vm update
  Object.defineProperties(local, {
    getters: {
      get: noNamespace
        ? () => store.getters
        : () => makeLocalGetters(store, namespace)
    },
    state: {
      get: () => getNestedState(store.state, path)
    }
  })

  return local
}

// 从store的getters中截取localContext需要的getter
function makeLocalGetters (store, namespace) {
  const gettersProxy = {}

  const splitPos = namespace.length
  Object.keys(store.getters).forEach(type => {
    // skip if the target getter is not match this namespace
    // 截取的依据是命名空间
    // 获取该module以及所有子module的getters
    if (type.slice(0, splitPos) !== namespace) return

    // extract local getter type
    const localType = type.slice(splitPos)

    // Add a port to the getters proxy.
    // Define as getter property because
    // we do not want to evaluate the getters in this time.
    Object.defineProperty(gettersProxy, localType, {
      get: () => store.getters[type],
      enumerable: true
    })
  })

  return gettersProxy
}

// 根据路径截取state
function getNestedState (state, path) {
  return path.length
    ? path.reduce((state, key) => state[key], state)
    : state
}

我个人认为比较难理解的是localGetters的实现。所有的getters都以hash的形式注册到了全局的getters上,要分辨属于哪个module只能通过module的命名空间和挂在全局getters上的键名来对比。在我的实现中获取localGetters比较容易,但是没考虑子module的getters。

关于module的还有注册各个属性,都比较好理解,唯一要说的是注册state:

Vue.set(parentState, moduleName, module.state)

之所以用了Vue.set而不是简单地设置parentState,是因为考虑到parentState如果是响应式的,子module的state可以自动变成响应式的(registerModule的时候会有这种情况)。在registerModule的时候,我们会发现会重新生成一个vue实例,它的作用仅仅是更新getters,原来的state并不受这个新vue实例的影响(进一步说,旧有的依赖也不会因为这个新vue实例而受影响)。

results matching ""

    No results matching ""