wabish / vuex-analysis

vuex 2.0 源码解读
MIT License
29 stars 1 forks source link

vuex 解读之 module #11

Open cobish opened 6 years ago

cobish commented 6 years ago

前言

store 将应用的状态集中起来,但如果应用变得非常复杂时,即状态非常的多时,store 就有可能变得相当臃肿。module 能够帮 store 划分了模块,每个模块都拥有自己的 state、getter、mutation、action 和 module。

那么 module 又是怎样进行划分的,划分后的模块又是如何管理自己的状态呢?接下来就来解读 module 的实现吧。

准备

解读前,需要对以下知识有所了解:

  1. Array.prototype.reduce()
  2. Vue.set()

解读

在 vuex 文档里有这么一句话:默认情况下,模块内部的 action、mutation 和 getter 是注册在全局命名空间的——这样使得多个模块能够对同一 mutation 或 action 作出响应。

什么意思呢?先看看以下示例:

const store = new Vuex.Store({
  state: {
    count: 0
  },
  mutations: {
    addNote () {
      console.log('root addNote')
    }
  },
  modules: {
    a: {
      state: {
        count: 0
      },
      mutations: {
        addNote () {
          console.log('module a addNote')
        }
      }
    }
  }
})

使用了 module 之后,state 则会被模块化。比如要调用根模块的 state,则调用 store.state.count,如果要调用 a 模块的 state,则调用 store.state.a.count

但是示例中的 mutation 则是注册在全局下的,即调用 store.commit('addNote'),将会调用跟模块和 a 模块的 mutation。除非区分各模块 mutation 的命名,否则,在同名的情况下,只要 commit 后就会被触发调用。

当然,vuex 2.0.0 后面的版本添加了命名空间 的功能,使得 module 更加的模块化。

所以接下来要解读的 module 中,实际上只要 state 是被模块化了, action、mutation 和 getter 还是在全局的模块下。

modules 的注册

installModule 里实现了 module 的注册,定位到 installModule 方法。

function installModule (store, rootState, path, module, hot) {
  const isRoot = !path.length
  const {
    modules
  } = module

  // set state
  if (!isRoot && !hot) {
    const parentState = getNestedState(rootState, path.slice(0, -1))
    const moduleName = path[path.length - 1]
    store._withCommit(() => {
      Vue.set(parentState, moduleName, state || {})
    })
  }

  // mutation 的注册
  // action 的注册
  // getter 的注册

  if (modules) {
    Object.keys(modules).forEach(key => {
      installModule(store, rootState, path.concat(key), modules[key], hot)
    })
  }
}

看到简化后的代码,可以看出 installModule 对 module 做了两步初始化操作。第一步是使用 Vue.set() 对当前的 module 的 state 设置了监听;第二步则是继续遍历子模块,然后递归调用 installModule。

set state

所以 modules 的核心实现就在于对当前的 module 的 state 设置了监听,将此段代码提取出来:

const parentState = getNestedState(rootState, path.slice(0, -1))
const moduleName = path[path.length - 1]
store._withCommit(() => {
  Vue.set(parentState, moduleName, state || {})
})

先猜测 getNestedState 方法可以获取到父 state。所以先取得父 state,再取得当前模块名称,最后使用 Vue.set() 将当前的 state 设置在父 state 下。实际上该实现就是在一个 vue 实例下为 data.state 添加属性,并能够使得 vue 实例能够监听到添加属性的改动。

getNestedState

const parentState = getNestedState(rootState, path.slice(0, -1))

通过 path.slice(0, -1) 将当前模块去掉,作为参数和 rootState 根状态传入 getNestedState 方法中,返回了当前模块的父状态 parentState。

来看看 getNestedState 的实现:

function getNestedState (state, path) {
  return path.length
    ? path.reduce((state, key) => state[key], state)
    : state
}

如果 length 等于 0,即只有根 state,直接返回。另一种情况,如果有嵌套的模块,那么通过 Array.prototype.reduce() 方法一直往根 state 的属性取 path 对应的 state 并返回。

至此,state 的模块化已经注册完成,然后递归调用 installModule 完成所有 module 的注册。

既然是往 rootState 里添加属性,那么获取则可以通过 store.state.a 来获取到模块,然后再继续获取模块里的 state。

get modules state

之前在解读 mutation 和 action 的时候,一直都将 getNestedState 这个方法给省略了。在注册 mutation 和 action 的时候,会出现以下这段代码:

getNestedState(store.state, path)

实际上这段代码就是获取当前 modules 的 state,然后作为参数回传。

存放数组

还记得解读 mutation 的时候,说到为什么会将 mutation 保存到了 store._mutations 数组里面。主要目的是将所有 module 里的 mutation 都存放在一个数组中,以便于在 commit 的时候能触发所有 mutation。

getter 和 action 用到数组存放也是这样一个原因。

但是,如果两个 module 里有相同的 mutation 名称,vuex 2.0.0 里做不到只触发其中一个 mutation。这个在往后的版本中设置命名空间可实现。

总结

本篇是对 module 的一个解读。注册 module 并没有想象中的那么复杂,主要分为两个步骤。

第一步是找到当前 module 的父 state,然后在其至少绑定当前 state 的监听,保证修改了 state 会触发相应。

第二步则是递归 module,保证设置子 module 的 state,从而实现 module 的子嵌套。