vuex源码分析(二)

660 阅读5分钟

二、模块初始化

在Store的constructor中,会执行一系列数据的初始化,其中会执行this._modules = new ModuleCollection(options),假设写入的option是这样的结构

const modulesA = {
  namespaced: true,
  state: {
    conut: 1
  },
  getters: {
    computedConut(state) {
      return state.conut + 1
    }
  },
  mutations: {
    add(state) {
      state.conut + 1
    }
  },
  actions: {
    addCount(context) {
      context.commit('add')
    }
  }
}
const store = new Vuex.Store({
  modules: {
    modulesA
  },
  state: {
    conut: 1
  },
  getters: {
    computedConut(state) {
      return state.conut + 1
    }
  },
  mutations: {
    add(state) {
      state.conut + 1
    }
  },
  actions: {
    addCount(context) {
      context.commit('add')
    }
  }
})

在执行ModuleCollection的constructor的时候,参数rawRootModule就是之前的options,constructor会执行this.register,第一参数传入空数组,第二个参数传入rawRootModule,第三个参数传false。在这里作者给出的注释是// register root module (Vuex.Store options)注册根部module。register函数首先会通过const newModule = new Module(rawModule, runtime),rawModule为rawRootModule,runtime为false。Module在执行constructor的时候会执行this._rawModule = rawModule来保存传入的rawModule, 执行const rawState = rawModule.state来保存rawModule为的state,也就是传入的对象根部的state,接着通过this.state = (typeof rawState === 'function' ? rawState() : rawState) || {}来保存state,也就是说根部的state既可以是对象的形式,也可以是一个函数执行后返回对象的形式。这样register函数的newModule就是一个Module实例。接着register函数会判断path的长度,path在首次执行传入的是一个空数组,也就是会执行this.root = newModule把我们实例化的newModule作为root。接着会判断rawModule.modules在上边的demo中,是有编写modules的,所以会通过Object.keys遍历rawModule.modules,去执行 this.register(path.concat(key), rawChildModule, runtime),path在第一次是一个空数组,但执行到了这一步,会通过concat,把遍历到的modules的key添加到数组中,这样path就成了['modulesA'],然后第二个参数为modulesA的值。这样递归的执行register函数进行注册,和第一次执行register不同的是,会先去执行const parent = this.get(path.slice(0, -1)),也就是执行get方法,由于对path进行了剔除最后一位的操作,所以传入的是一个空数组。get函数会对传入的path做一个reduce,reduce每次执行会把当前的key作为参数传入Module实例的getChild方法,执行并返回结果,Module实例的getChild方法会返回当前this._children[key]。对于本次执行get由于传入的path参数是一个空数组,所以会直接返回this.root,那么parent就是第一次执行registerModule实例。接着本次的register会执行parent.addChild(path[path.length - 1], newModule),第一个参数传入path的最后一位,也就是moduleA字符串,第二个参数传入的是本次register的Module实例,addChild会执行this._children[key] = module这样就把ModuleA创建的Module实例存入了根部Module实例的_children中,那么假如modulesA的modules执行到get方法的时候,就可以拿到modulesA的module实例作为parent,继续执行appendchild操作,这样就递归建立了Module实例之间的父子关系。

// src/module/module-collection.js
class ModuleCollection {
  constructor (rawRootModule) {
    // register root module (Vuex.Store options)
    this.register([], rawRootModule, false)
  }
  get (path) {
    return path.reduce((module, key) => {
      return module.getChild(key)
    }, this.root)
  }
  ...
  register (path, rawModule, runtime = true) {
    ...
    const newModule = new Module(rawModule, runtime)
    if (path.length === 0) {
      this.root = newModule
    } else {
      const parent = this.get(path.slice(0, -1))
      parent.addChild(path[path.length - 1], newModule)
    }

    // register nested modules
    if (rawModule.modules) {
      forEachValue(rawModule.modules, (rawChildModule, key) => {
        this.register(path.concat(key), rawChildModule, runtime)
      })
    }
  }
  ...
}
// src/module/module.js
export default class Module {
  constructor (rawModule, runtime) {
    this.runtime = runtime
    // Store some children item
    this._children = Object.create(null)
    // Store the origin module object which passed by programmer
    this._rawModule = rawModule
    const rawState = rawModule.state

    // Store the origin module's state
    this.state = (typeof rawState === 'function' ? rawState() : rawState) || {}
  }
  ...
  getChild (key) {
    return this._children[key]
  }
  ...
}

三、模块安装

执行new ModuleCollection(options)会为store实例的_modules挂载,之后会执行installModule函数,作者给出的注释是// init root module.,// this also recursively registers all sub-modules以及// and collects all module getters inside this._wrappedGetters,也就是说注册根部模块并且递归的安装所有子模块,并且会把所有模块的getter收集到this._wrappedGetters中。

1、执行makeLocalContext拿到local

执行installModule的时候第一个参数传入当前的store实例,第二个参数传入根部的state,第三个参数传入空数组,第四个参数传入根部的module实例。installModule函数首先执行const isRoot = !path.length,根据传入的path.length(首次是空数组)判断他是不是root,然后执行 const namespace = store._modules.getNamespace(path)getNamespace函数会根据传入的path数组,获取他们的module,如果namespaced为true,那么会返回key+/,假如传入的path为['modulesA','modulesB'],并且两者namespaced都为true,则会返回modulesA/modulesB/,首次执行传入的path为空数组,所以首次namespace为一个空的字符串。接着判断如果第四个参数modulenamespaced为true,那么会执行store._modulesNamespaceMap[namespace] ,也就是说store._modulesNamespaceMap对象中存储的是namespaced为true的module实例,并且以namespace作为key。接着会执行const local = module.context = makeLocalContext(store, namespace, path)makeLocalContext函数整体看下来定义了一个local对象,其中有dispatchcommit,接通过Object.defineProperties为local定义了访问getters和state的返回结果,最终返回local。dispatch和commit都会判断Namespace,如果不是一个空的字符串,也就是说module的namespaced为false,会直接注册,如果有Namespace那么会执行type = namespace + type,也就是说在vuex中注册的带有namesped的module中的mutationsactions最终访问的方式会是modulesA/modulesB/methods,也就是说makeLocalContext函数为我们重写了commit和dispatch。之后定义的getter的访问方式同样也是通过Namespace来判断,如果有,则会执行makeLocalGettersmakeLocalGetters会判断store._makeLocalGettersCache[namespace],也就是是否有了当前module的getter缓存,如果没有则会遍历getters,首先拿到key最后的getter名称,然后通过 Object.defineProperty把当前的getter和全局的getter进行绑定,这样当访问全局的比如modulesA/modulesB/xxxx就会访问到当前module的getter。state的访问方式和之前的不同,他是通过state.ModulesA.ModulesB.xxx进行访问,对应getNestedState函数,去执行return path.reduce((state, key) => state[key], state),这样当访问子模块的state,会一层层去找,最终返回访问module的state。这样在配置store的时候,我们无需去关注他的嵌套层级。

// src/store.js
function installModule (store, rootState, path, module, hot) {
  const isRoot = !path.length
  const namespace = store._modules.getNamespace(path)
  // register in namespace map
  if (module.namespaced) {
    ...
    store._modulesNamespaceMap[namespace] = module
  }
  ...
  const local = module.context = makeLocalContext(store, namespace, path)
  ...
}

/**
 * make localized dispatch, commit, getters and state
 * if there is no namespace, just use root ones
 */
function makeLocalContext (store, namespace, path) {
  const noNamespace = namespace === ''

  const local = {
    dispatch: noNamespace ? store.dispatch : (_type, _payload, _options) => {
      ...
      if (!options || !options.root) {
        type = namespace + type
        ...
      }
      return store.dispatch(type, payload)
    },
    commit: noNamespace ? store.commit : (_type, _payload, _options) => {
      ...
      if (!options || !options.root) {
        type = namespace + type
        ...
      }
      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
}

function makeLocalGetters (store, namespace) {
  if (!store._makeLocalGettersCache[namespace]) {
    const gettersProxy = {}
    const splitPos = namespace.length
    Object.keys(store.getters).forEach(type => {
      // skip if the target getter is not match this namespace
      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
      })
    })
    store._makeLocalGettersCache[namespace] = gettersProxy
  }

  return store._makeLocalGettersCache[namespace]
}