Vuex源码简要分析

75 阅读7分钟

「这是我参与2022首次更文挑战的第3天,活动详情查看:2022首次更文挑战」。

vuex初始化

一般使用Vuex的步骤如下:

import Vuex from 'vuex'
Vue.use(Vuex)
const store = new Vuex.Store({
  modules: {
    a: moduleA,
    b: moduleB
  }
})
new Vue({
	el: '#app',
  store
})

Vue.use(Vuex)调用分析

Vue.use(Vuex)同vue-router一样,调用的是install方法(可见这里)。主要定义如下:

export function install (_Vue) {
  if (Vue && _Vue === Vue) {
    if (__DEV__) {
      console.error(
        '[vuex] already installed. Vue.use(Vuex) should be called only once.'
      )
    }
    return
  }
  Vue = _Vue
  applyMixin(Vue)
}

将传入的Vue实例对象重新传入applyMixin函数, applyMixin函数定义在mixin.js中。

export default function (Vue) {
  const version = Number(Vue.version.split('.')[0])

  if (version >= 2) {
    Vue.mixin({ beforeCreate: vuexInit })
  } else {
    // override init and inject vuex init procedure
    // for 1.x backwards compatibility.
    // ....
  }
  function vuexInit () {
    const options = this.$options
    // store injection
    if (options.store) {
      this.$store = typeof options.store === 'function'
        ? options.store()
        : options.store
    } else if (options.parent && options.parent.$store) {
      this.$store = options.parent.$store
    }
  }
}

兼容Vue1.0版本的不做分析,Vue2.x同vue-router大致相同,向Vue实例全局beforeCreate混入函数vuexInit,使在Vue组件里通过this.$store能访问到Vuex实例。

Vuex实例初始化分析

在使用Vuex时,会通过new Vuex.store({}) 将Vuex实例传入Vue实例中,从而让Vue实例中访问this.options能访问到Vuex实例,从而在上文中实现this.options**能访问到Vuex实例,从而在上文中实现**this.store访问到Vuex实例。 Vuex.store方法定义在store.js文件中。主要是对我们传入的options进行初始化,一般传入的多为modules,如下:

const moduleA = {
  state: () => ({ ... }),
  mutations: { ... },
  actions: { ... },
  getters: { ... }
}
const moduleB = {
  state: () => ({ ... }),
  mutations: { ... },
  actions: { ... }
}
const store = new Vuex.store({
  modules: {
    a: moduleA,
    b: moduleB
  }
}

以上面的options为例,从初始化模块,安装模块和初始化store._vm分析。

初始化模块

export class Store {
  constructor(options = {}) {
    // ...
    this._modules = new ModuleCollection(options)
    const state = this._modules.root.state
    installModule(this, state, [], this._modules.root)
  }
}

Store类的初始化中,通过实例化ModuleCollection实现。

ModuleCollection定义在module-collection文件中,具体内容如下:

export default 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) {
    if (__DEV__) {
      assertRawModule(path, rawModule)
    }

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

当实例化ModuleCollection时,会调用this.register方法将我们定义的options传入,register方法的主要逻辑如下:

  • register方法会实例化Module,当初始化时path的长度为0,会将实例化Module赋值给this.root。
  • 如果我们定义的options有子modules,会递归调用this.register
  • 当递归调用this.register时path不为空,会通过this.get方法传入path参数获取到当前moduler的父级Module实例并调用addChildren方法,添加Module实例的父子关系。

通过这一系列的操作,最终this.root会构造成一颗Module实例树,与我们传入的modules相对应。

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]
  }
  addChild (key, module) {
    this._children[key] = module
  }
}

它主要作用如下:

  • 对我们传入的每一个module赋值给this._rawModule,之后就可以通过Module实例中的this._rawModule获取到我们每一个module所配置的getters.actions,mutation等。
  • 将我们每一个module所定义的state赋值给this.state,需要注意的是如果state为函数时,会执行后再赋值。

综上,模块初始化的主要作用是将我们传入的options根据module层级关系生成一颗Module实例树,在Store实例中能通过this._modulesrrr的root属性访问到,每一个Module实例具有访问到当前module所定义的state,getters,actions,muation的方法和属性。

安装模块分析

安装模块同初始化模块一样,也定义在store.js文件中

export class Store {
  constructor (options = {} ) {
    // ....
    this._modules = new ModuleCollection(options)
    const state = this._modules.root.state
    installModule(this. state, [], this._modules.root)
  }
}

安装模块通过installModule方法实现,传入Store的this指向, 根Module实例的state属性,空的path数组, 根Module实例。

installModule同样定义在store.js文件中,它主要实现的功能如下:

  • 构建_modulesNamespaceMap

    const namespace = store._modules.getNamespace(path)
    // ...
    // register in namespace map
    if (module.namespaced) {
      store._modulesNamespaceMap[namespace] = module
    }
    

    获取当前path(即当前module实例)所对应的命名空间字符串, store._modules.getNamespace(path)方法定义在module-collection.js文件中。

    getNamespace (path) {
        let module = this.root
      return path.reduce((namespace, key) => {
        module = module.getChild(key)
        return namespace + (module.namespaced ? key + '/' : '')
      }, '')
    }
    

    this.root是在初始化模块时所构建的Module实例树,它通过reduce方法一层层的查找path所对应的Module实例并获取到它的namespaced属性(返回true或false)。然后返回path数组所对应的namespace字符串。该字符串表征的是当前Module实例所对应的namespace

    在instllModule方法中,将获取到的namespace字符串保存到Store实例中的_modulesNamespaceMap中,建立namespaces与其对应Moduel实例的字典。

  • 构建一个本地上下文环境

      const local = module.context = makeLocalContext(store, namespace, path)
    

    makeLocalContext方法定义在这里,它的作用是根据当前Module实例是否具有命名空间,来定义dispath,commit,get,state方法。如果没有命名空间则调用Store实例所定义的相关方法。有就将传入的type加上namespace再调用Store实例所定义的相关方法并传入type参数。

  • 注册Module相关实例方法

    module.forEachMutation((mutation, key) => {
      const namespacedType = namespace + key
      registerMutation(store, namespacedType, mutation, local)
    })
    
    module.forEachAction((action, key) => {
      const type = action.root ? key : namespace + key
      const handler = action.handler || action
      registerAction(store, type, handler, local)
    })
    
    module.forEachGetter((getter, key) => {
      const namespacedType = namespace + key
      registerGetter(store, namespacedType, getter, local)
    })
    

    调用Module实例的方法,对我们传入options的mutaion,action,getter方法进行依次注册。注册方法大致相同。

    • registerMutation方法

        function registerMutation (store, type, handler, local) {
        const entry = store._mutations[type] || (store._mutations[type] = [])
          entry.push(function wrappedMutationHandler (payload) {
          handler.call(store, local.state, payload)
        })
        }
      

      将定义的每一个mutation函数包裹到wrappedMutationHandler函数中并已type为key,添加到Store实例中的_mutations[type]数组中,方便后续调用。

    • registerAction方法

      registerAction方法与registerMutation方法大致相同,不过会确保返回的wrappedActionHandler是一个异步函数

      function wrappedActionHandler (payload) {
        let res = handler.call(store, {
          dispatch: local.dispatch,
          commit: local.commit,
          getters: local.getters,
          state: local.state,
          rootGetters: store.getters,
          rootState: store.state
        }, payload)
      if (!isPromise(res)) {
        res = Promise.resolve(res)
      }
      // ...
      return res
      }
      
    • registerGetter方法

      函数会在wrappedGetter函数中返回rawGetter, rawGetter是我们在options中定义的getters函数。

      function wrappedGetter (store) {
        return rawGetter(
          local.state, // local state
          local.getters, // local getters
          store.state, // root state
          store.getters // root getters
        )
      }
      
      
  • 递归调用installModule

    最后installModule会递归调用自身,将之前构建的Module实例依次执行installModule方法。

    module.forEachChild((child, key) => {
        installModule(store, rootState, path.concat(key), child, hot)
    })
    
    

    在installModule方法中,如果是递归调用的,会执行下面的逻辑:

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

    获取到父Module实例所对应的state,并通过Vue.set设置parentState对象,使其能通过path访问到子Module实例的state。

综上整个安装模块的主要过程是对Module实例树递归调用递归调用installModule方法,构建Store实例的_mutations,_actions,_wrappedGetters字典,使其与我们定义的options中的方法一一对应。并且使其能通过根Module根实例的state访问到所有的Module实例中的state属性。

初始化Store._vm

Store实例化中,会调用以下代码执行初始化Store._vm

resetStoreVM(this, state)
function resetStoreVM (store, state, hot) {
  // ...
  const oldVm = store._vm
  // bind store public getters
  store.getters = {}
  // reset local getters cache
  store._makeLocalGettersCache = Object.create(null)
  const wrappedGetters = store._wrappedGetters
  const computed = {}
  forEachValue(wrappedGetters, (fn, key) => {
    computed[key] = partial(fn, store)
    Object.defineProperty(store.getters, key, {
      get: () => store._vm[key],
      enumerable: true // for local getters
    })
  })
  store._vm = new Vue({
    data: {
      $$state: state
    },
    computed
  })
}

resetStoreVM的主要作用是创建一个新的Vue实例store._vm,通过在该实例中定义computed和data,建立store.getters和store.state之间的联系。当我们访问Store实例的state时,实际会访问到store._vm._data.$$state

  get state () {
    return this._vm._data.$$state
  }

而在上文中store_wrappedGetters会传入store.state:

store._wrappedGetters[type] = function wrappedGetter (store) {
    return rawGetter(
      local.state, // local state
      local.getters, // local getters
      store.state, // root state
      store.getters // root getters
    )
  }

这样当store.state发生改变时,通过computed方法定义的getters也会重新计算。从而构建起getters和state的联系。

相关API调用流程分析

主要的API包括commit,getters,dispatch等,主要都是对数据的获取和存储。

数据获取

  • 通过store.state获取 在上文中,我们知道访问store.state最终会访问到this._modules.root.state

    get state () {
        return this._vm._data.$$state
    }
    
    function resetStoreVM(store, state, hot) {
      // ...
      store._vm = new Vue({
        data: {
          $$state: state
        }
      })
    }
    
    const state = this._modules.root.state
    installModule(this, state, [], this._modules.root)
    resetStoreVM(this, state)
    

    而this._modules.root.state在installModule里,我们已经将它与作为path的module name绑定起来,所以可以通过store.state.a.b.XXX的形式去获取数据。

    const parentState = getNestedState(rootState, path.slice(0, -1))
    const moduleName = path[path.length - 1]
    Vue.set(parentState, moduleName, module.state
    
  • 通过store.getters获取

    当我们通过option将getters传入时,从上文可以得知会在installModule时注册

    module.forEachGetter((getter, key) => {
      const namespacedType = namespace + key
      registerGetter(store, namespacedType, getter, local)
    })
    function registerGetter (store, type, rawGetter, local) {
      store._wrappedGetters[type] = function wrappedGetter (store) {
        return rawGetter(
          local.state, // local state
          local.getters, // local getters
          store.state, // root state
          store.getters // root getters
        )
      }
    }
    

    rawGetter就是我们传入的getters函数,它会传入state和getters,从而能够访问到store.state里的数据。

数据存储

  • mutation提交

    主要是通过commit去提交一个mutation的形式去提交修改state,我们定义的mutation函数通过option传入后最终会存储在store._mutations并传入state方便修改数据。

    function registerMutation (store, type, handler, local) {
      const entry = store._mutations[type] || (store._mutations[type] = [])
      entry.push(function wrappedMutationHandler (payload) {
        handler.call(store, local.state, payload)
      })
    }
    

    当我们调用commit去提交一个mutation时,会根据我们传入的type去查找store._mutations里对应的mutations函数并执行

    commit (_type, _payload, _options) {
      // check object-style commit
      const {
        type,
        payload,
        options
      } = unifyObjectStyle(_type, _payload, _options)
      const mutation = { type, payload }
      this._withCommit(() => {
        entry.forEach(function commitIterator (handler) {
          handler(payload)
        })
      })
    }
    
  • action提交

    主要是通过action提交一个mutation来修改数据,他与mutation的不同主要是异步操作,它也在installModule里将其存储在store._mutations中。之后我们可以通过dispatch函数执行它。

    dispatch (_type, _payload) {
      // check object-style dispatch
      const {
        type,
        payload
      } = unifyObjectStyle(_type, _payload)
    
      const action = { type, payload }
      const entry = this._actions[type]
    
      const result = entry.length > 1
        ? Promise.all(entry.map(handler => handler(payload)))
        : entry[0](payload)
    
      return new Promise((resolve, reject) => {
        result.then(res => {
          // ...
          resolve(res)
        }, error => {
          // ...
          reject(error)
        })
      })
    }
    

    同commit的逻辑大致一致,所不同的是它是异步的。

参考资料

总结

主要分析了Vuex如何初始化和安装我们的module以及当调用commit,dispatch时访问并state的具体原理。