Skip to content

vuex概念及部分源码分析 #8

@evenMai92

Description

@evenMai92

Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式。它采用集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化。下面我们针对以下几点,对源码进行分析:

  1. Vuex这个包对外暴露哪些东西,其用处是什么?
  2. Vuex怎么初始化及挂载,以及和vue组件关联起来?
  3. Vuex怎么实现不同module下,state、getters、mutations、actions等属性的获取?
  4. Vuex怎么做到限制state的更新要经过commit相应的mutations,要不然报错?
  5. Vuex和redux、mobx等状态管理库一样,本身都是同步更新的吗?

一. Vuex暴露的方法和属性及其用处

// src/index.js
export default {
  Store,
  install,
  version: '__VERSION__',
  mapState,
  mapMutations,
  mapGetters,
  mapActions,
  createNamespacedHelpers
}

可以看到对外暴露Store对象、install挂载方法、version版本、以及方便在组件中获取state、mutation、getter和action的方法和初始化时用于创建命名空间别名的createNamespacedHelpers;

二. Vuex的挂载及如何与组件关联

Vuex作为Vue的插件,通过提供install方法来挂载到Vue实例上。

// src/mixin.js 导出作为install方法
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.
    const _init = Vue.prototype._init
    Vue.prototype._init = function (options = {}) {
      options.init = options.init
        ? [vuexInit].concat(options.init)
        : vuexInit
      _init.call(this, options)
    }
  }

  /**
   * Vuex init hook, injected into each instances init hooks list.
   */

  function vuexInit () {
    const options = this.$options
    // store injection
    if (options.store) {
     // 根组件挂载$store
      this.$store = typeof options.store === 'function'
        ? options.store()
        : options.store
    } else if (options.parent && options.parent.$store) {
     // 从父组件获取$store
      this.$store = options.parent.$store
    }
  }
}

可以看出来,是通过调用了Vue.mixin在全局里混入了beforeCreate生命周期,那么Vue实例在初始化触发beforeCreate生命周期时,就会执行vuexInit方法。然后将我们在初始化配置Vue实例时,传入的store实例挂载到每个组件的$store上,也就是说,我们可以在任何组件里,都能通过this.$store取到相应的state状态值。

const store = new Vuex.Store({
  state: {},
  mutations: {}
});
const app = new Vue({
  el: '#app',
  // 把 store 对象提供给 “store” 选项,这可以把 store 的实例注入所有的子组件
  store
})

三. Vuex初始化

export default Store {
  constructor (options = {}) {
    // Auto install if it is not done yet and `window` has `Vue`.
    // To allow users to avoid auto-installation in some cases,
    // this code should be placed here. See #731
    if (!Vue && typeof window !== 'undefined' && window.Vue) {
      install(window.Vue)
    }
   // ....
    const {
      plugins = [],
      strict = false
    } = options

    // store internal state
    this._committing = false
    this._actions = Object.create(null) // 用于存储配置的actions
    this._actionSubscribers = []
    this._mutations = Object.create(null) // 用于存储配置的mutations
    this._wrappedGetters = Object.create(null) // 用于存储配置的getters
    this._modules = new ModuleCollection(options)  // 实例化后会将我们在store初始化配置的mudule构建成属性,eg:{root: {_children: {a: module,  b: module}, module}}
    this._modulesNamespaceMap = Object.create(null) // 用去存储每个命令空间的模块。eg: {'a/': module, 'b/': module}
    this._subscribers = []
    this._watcherVM = new Vue()

    // bind commit and dispatch to self
    const store = this
    const { dispatch, commit } = this
   // 重新绑定dipatch和commit方法,强行将其执行上下文this指向store,避免用户使用时,改写this指向,导致出错
    this.dispatch = function boundDispatch (type, payload) {
      return dispatch.call(store, type, payload)
    }
    this.commit = function boundCommit (type, payload, options) {
      return commit.call(store, type, payload, options)
    }

    // strict mode
    this.strict = strict

    const state = this._modules.root.state

    //  根据命令空间与否注册所有模块的state,mutation,getter和action(重点)
    // init root module.
    // this also recursively registers all sub-modules
    // and collects all module getters inside this._wrappedGetters
    installModule(this, state, [], this._modules.root)

   // 将state和getters变成响应式(重点)
    // initialize the store vm, which is responsible for the reactivity
    // (also registers _wrappedGetters as computed properties)
    resetStoreVM(this, state)

    // apply plugins
    plugins.forEach(plugin => plugin(this))

    if (Vue.config.devtools) {
      devtoolPlugin(this)
    }
  }

 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
   }

   // 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, module.state)
     })
   }

   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) => {
    installModule(store, rootState, path.concat(key), child, hot)
   })
 }
   /**
   * 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) => {
        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
          }
        }

        return store.dispatch(type, payload)
      },

      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
  }

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

    // bind store public getters
    store.getters = {}
    const wrappedGetters = store._wrappedGetters
    const computed = {}
    forEachValue(wrappedGetters, (fn, key) => {
      // use computed to leverage its lazy-caching mechanism
      computed[key] = () => 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
    store._vm = new Vue({
      data: {
        $$state: state
      },
      computed
    })
    Vue.config.silent = silent

    // enable strict mode for new vm
    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())
    }
  }
}

初始化时通过调用installModule方法,注册state、mutation、getter、action。在注册的过程中,会拼接上相应的命名空间。而makeLocalContext 方法将会构造不同的访问方式,当没有命名空间时,用默认的commit和action;有命名空间时,使用改造过的commit和action,其里面会拼接上命名空间的值去查找相应的mutation和action(getter和state也类似),因为我们在开发中,可以直接在组件中store.commit('increment')或模块中commit('increment'),内部会拼接上命令空间找到相应的方法,无需我们处理。

最后调用resetStoreVM将我们的state和getter设置到vue实例上,变成响应式。因此当我们在组件中访问state时,其实访问的是$$state数据,这样就会触发Vue的依赖收集,改变后也就会触发组件更新。

四. vuex更新数据的方式

Vuex异步请求放在Action处理,Action 提交的是 mutation,而不是直接变更状态,更新数据直接通过commit来操作(必须是同步函数),目的是为了devtools 能捕捉到前一状态和后一状态的快照,然后进行回溯。enableStrictMode会对state进行深度监听,如果_committing值为false,则报错提示,其中_committing可以通过_withCommit函数设置(只能提示,不能拦截掉,也就是还可以强制更新)

 function enableStrictMode (store) {
  // 严格模式且开发环境下,深度监听state值的变化,如果_committing为false,则报错
  store._vm.$watch(function () { return this._data.$$state }, () => {
    if (process.env.NODE_ENV !== 'production') {
      assert(store._committing, `Do not mutate vuex store state outside mutation handlers.`)
    }
  }, { deep: true, sync: true })
}
 _withCommit (fn) {
   const committing = this._committing
   this._committing = true
   fn()
   this._committing = committing
 }

五. vuex数据更新是异步还是同步?

从之前的源码中可以看到,Vuex本身其实是同步更新的,但是在Vue中表现出异步是因为将state的数据挂载做为Vue的响应式data数据处理,满足Vue的数据更新机制(异步更新)。

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions