Vuex 源码分析

假设你已经配置了 store 文件,并且执行了 Vue.use(Vuex),本文将从这里开始进行分析

Vue.use 的时候发生了什么?

vuex 默认导出了 install 方法,

// 省略了一些文件导入,只看关键部分
export default {
  Store,
  install,
  version: '__VERSION__',
  mapState,
  mapMutations,
  mapGetters,
  mapActions,
  createNamespacedHelpers,
}

在执行 Vue.use 的时候其实就是在调用 install

export function install(_Vue) {
  // 只允许一次vuex的注入
  if (Vue && _Vue === Vue) {
    if (process.env.NODE_ENV !== 'production') {
      console.error(
        '[vuex] already installed. Vue.use(Vuex) should be called only once.'
      )
    }
    return
  }
  Vue = _Vue
  applyMixin(Vue) // 这里是什么呢?
}

applyMixin

紧接着调用了 applyMixin,这里是混入了一个生命周期 beforeCreate 的钩子,在这个生命周期里进行了 vuexInit

Vue.mixin({ beforeCreate: vuexInit }) // 将vuexInit注册到beforeCreate这个生命周期,在new Vue挂载后在
// 对应的生命周期中执行vuexInit,此时new Vuex.Store已经初始化完成

// 这里是 vuexInit 所做的事情
 function vuexInit () {
    const options = this.$options
    // store injection
    if (options.store) {
      this.$store = typeof options.store === 'function'
        ? options.store() // 如果new Vue传入的store参数是个函数则立即执行
        : options.store
    } else if (options.parent && options.parent.$store) {
      // 保证每个实例化的vue都能通过 this.$store拿到store数据
      this.$store = options.parent.$store
    }
  }
}

new Store

接下来分析我们 new Store 中的数据是怎么建立层级关系的,我们可能将 store 拆分成多个小的 store 然后在根 store 中通过 module 定义将小的 store 注册进来,那么他们通过什么建立联系的呢?

首先每个模块都会有一个自己的实例,定义在 module.js 中

class Module {
  constructor(rawModule, runtime) {
    this.runtime = runtime
    // Store some children item
    this._children = Object.create(null) // 保存子module的对象,用于和子module建立关系
    // Store the origin module object which passed by programmer
    this._rawModule = rawModule // 当前实例的module
    const rawState = rawModule.state

    // Store the origin module's state
    this.state = (typeof rawState === 'function' ? rawState() : rawState) || {}
  }

  get namespaced() {
    return !!this._rawModule.namespaced
  }

  // 省略一些其余方法定义
}

module-collection

module 实例的初始化以及通过命名空间建立索引定义在 module-collection.js 中,这里截取一个主要的方法

// path是根据命名空间形成的数组,默认为空数组,此时代表注册进来的为根module
register (path, rawModule, runtime = true) {
    if (process.env.NODE_ENV !== 'production') {
      assertRawModule(path, rawModule)
    }

    // 实例化module
    const newModule = new Module(rawModule, runtime)
    if (path.length === 0) {
      // 建立根module
      this.root = newModule
    } else {
      // 从根module通过getChild一层一层找到当前module的父级
      const parent = this.get(path.slice(0, -1))
      // 与子module建立父子关系
      parent.addChild(path[path.length - 1], newModule)
    }

    // register nested modules
    if (rawModule.modules) {
      // 如果当前注册进来的module还有子module,则递归注册
      forEachValue(rawModule.modules, (rawChildModule, key) => {
        this.register(path.concat(key), rawChildModule, runtime)
      })
    }
  }

installModule

接下来是最关键的部分 installModule,这里是为什么不能通过其他方式修改 store 数据的原因

// init root module.
// this also recursively registers all sub-modules
// and collects all module getters inside this._wrappedGetters
installModule(this, state, [], this._modules.root)

这个函数整体比较长,这里会在关键地方加一下注释

function installModule(store, rootState, path, module, hot) {
  const isRoot = !path.length
  // 这里获取根据module定义的名字拼接而成的命名空间
  const namespace = store._modules.getNamespace(path)

  // register in namespace map
  if (module.namespaced) {
    if (
      store._modulesNamespaceMap[namespace] &&
      process.env.NODE_ENV !== 'production'
    ) {
      console.error(
        `[vuex] duplicate namespace ${namespace} for the namespaced module ${path.join(
          '/'
        )}`
      )
    }
    store._modulesNamespaceMap[namespace] = module
  }

  // set state
  if (!isRoot && !hot) {
    const parentState = getNestedState(rootState, path.slice(0, -1))
    // 当前module的命名空间
    const moduleName = path[path.length - 1]
    store._withCommit(() => {
      if (process.env.NODE_ENV !== 'production') {
        if (moduleName in parentState) {
          console.warn(
            `[vuex] state field "${moduleName}" was overridden by a module with the same name at "${path.join(
              '.'
            )}"`
          )
        }
      }

      // 这里将子module的state注册到父级state对象下
      Vue.set(parentState, moduleName, module.state)
    })
  }

  // 关键是这里,makeLocalContext对store中定义的数据和操作进行了进一步的约束和封装
  const local = (module.context = makeLocalContext(store, namespace, path))

  // 下面四个函数就是注册相应的事件
  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.forEachChild((child, key) => {
    // 这里会对子module递归注册,因此会构建出每个makeLocalContext的值,进而对每个dispatch形成命名空间的嵌套
    installModule(store, rootState, path.concat(key), child, hot)
  })
}

makeLocalContext

首先看一下 makeLocalContext 做了哪些事情,这里挑关键点进行分析

  dispatch: noNamespace ? store.dispatch : (_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._actions[type]) {
          console.error(`[vuex] unknown local action type: ${args.type}, global type: ${type}`)
          return
        }
      }

    // 这里是对dispatch的一层封装,实际上调用的是存在namespace的,在registerAction的时候会按照namespace将
    // action注册到action的entry数组中,因此这里dispatch回去对应的数组中匹配
      return store.dispatch(type, payload)
    },

这里截取一个 registerAction 为例

function registerAction(store, type, handler, local) {
  // 这里的type是已经包含命名空间的
  const entry = store._actions[type] || (store._actions[type] = [])
  entry.push(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)
    }
    if (store._devtoolHook) {
      return res.catch((err) => {
        store._devtoolHook.emit('vuex:error', err)
        throw err
      })
    } else {
      return res
    }
  })
}

resetStoreVM

通过以上操作我们已经知道 store 树的构建过程,以及每一级通过命名空间建立父子关系,那么 store 的数据是如何建立起响应式的呢?答案是 resetStoreVM 这个函数,我会在这个函数关键部分添加注释

function resetStoreVM(store, state, hot) {
  const oldVm = store._vm

  // bind store public getters
  // 这里是外部可以挺过store.getters获取定义的getters的原因,因为在这里吧getters定义了
  store.getters = {}
  // reset local getters cache
  store._makeLocalGettersCache = Object.create(null)
  // 这里是从rawGetters拿到我们顶一个getter
  const wrappedGetters = store._wrappedGetters
  const computed = {}
  forEachValue(wrappedGetters, (fn, key) => {
    // use computed to leverage its lazy-caching mechanism
    // direct inline function use will lead to closure preserving oldVm.
    // using partial to return function with only arguments preserved in closure environment.
    // 这个累了的key就是已经根据命名空间建立好层级关系的key
    computed[key] = partial(fn, store)
    // 这里是做了响应式的定义
    Object.defineProperty(store.getters, key, {
      get: () => store._vm[key], // 拿到的数据也是响应式的
      enumerable: true, // for local getters
    })
  })

  // use a Vue instance to store the state tree
  // suppress warnings just in case the user has added
  // some funky global mixins
  const silent = Vue.config.silent
  Vue.config.silent = true
  // 定义一个vue实例,将计算属性和state放入,用于store._vm[key]可以响应式
  store._vm = new Vue({
    data: {
      $$state: state,
    },
    computed,
  })
  Vue.config.silent = silent

  // enable strict mode for new vm
  // 这个函数主要是对非commit操作state的更改做出警告
  if (store.strict) {
    enableStrictMode(store)
  }

  if (oldVm) {
    if (hot) {
      // dispatch changes in all subscribed watchers
      // to force getter re-evaluation for hot reloading.
      store._withCommit(() => {
        oldVm._data.$$state = null
      })
    }
    Vue.nextTick(() => oldVm.$destroy())
  }
}