最新公告
  • 欢迎您光临起源地模板网,本站秉承服务宗旨 履行“站长”责任,销售只是起点 服务永无止境!立即加入钻石VIP
  • Vuex 源码浅析(概览)

    正文概述 掘金(cherish553)   2021-01-27   761

    整体概念

    import Vue from "vue";
    import Vuex from "vuex";
    import App from './App.vue'
    
    Vue.use(Vuex);
    
    const store = new Vuex.Store({
      state: {
        count: 0,
      },
      mutations: {
        increment(state) {
          state.count++;
        },
      },
    });
    new Vue({
      el: '#app',
      render: h => h(App)
      store
    })
    

    这是一个简单的使用的例子,在项目的开始我们需要

    1. 引入vuex,接着通过Vue.use(vuex)进行插件的注册
    2. 通过new Vuex.Store创建 store 实例
    3. 把 store 作为 options 的一项配置,最终传入_init函数中

    通过 vue.use 进行挂载

    vue.use 是什么

    Vue.use定义在src/core/global-api/use.js文件下,是 vue 通过initGlobalAPI函数中的initUse(Vue)实现了 Vue 构造函数静态方法的挂载。

    Vue.use函数分为两个部分,第一个部分是防止重复注册,第二个部分是注册 plugin。

    • 防止重复注册

      首次进入他会判断this._installedPlugins如果没有的话,则把这个属性初始化为一个空数组,接着通过我们传入的plugin参数判断是否存在于this._installedPlugins,如果存在则返回 this,不进行注册。

      • 在这里 this 的指向是 vue 构造函数,也就是说,当我们使用 vue.use 最终返回的仍然是 vue 构造函数,这代表了 vue.use 是可以支持链式操作的。

      • 如果找到了之前我们已经注册过这个 plugin,那么不会再次进行注册,防止多次注册。

    • 注册 plugin

      首先通过toArray函数拿到 arguments 数组,接着通过 unshift 把 this(vue 构造函数)作为数组的第一项,接着他会判断,传入的plugin的 install 是否是一个函数,如果是一个函数则直接执行,并把新生成的 args 作为参数传入,否则如果 plugin 是一个函数则直接执行。然后把传入的pluginpush 到this._installedPlugins,作为注册过的缓存。

    Vue.use = function (plugin: Function | Object) {
      const installedPlugins =
        this._installedPlugins || (this._installedPlugins = []);
      if (installedPlugins.indexOf(plugin) > -1) {
        return this;
      }
    
      // additional parameters
      const args = toArray(arguments, 1);
      args.unshift(this);
      if (typeof plugin.install === "function") {
        plugin.install.apply(plugin, args);
      } else if (typeof plugin === "function") {
        plugin.apply(null, args);
      }
      installedPlugins.push(plugin);
      return this;
    };
    

    install 函数

    vuex 的 install 函数定义在src/store.js下,首先他会判断Vue && _Vue === Vue_Vue是通过 use 之后,传入的 vue,Vue是 vuex 定义的全局变量。在调用install函数的时候,会先进行赋值操作Vue = _Vue,这样全局任何地方都可以通过 Vue 拿到 vue 的构造函数。对应上边的判断,如果已经赋值,则说明已经通过Vue.use进行了注册,如果注册过,则会 return,并且报出'[vuex] already installed. Vue.use(Vuex) should be called only once.',这样防止用户重复通过 use 注册。install 最后会执行applyMixin(Vue)

    // src/store.js
    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);
    }
    

    applyMixin 函数

    applyMixin函数定义在src/mixin.js。他首先拿到 Vue 的版本,如果版本是小于 2 的,那么会把vuexInit函数,作为调用_init的参数传入。如果是大于等于 2 版本,则通过Vue.mixinvuexInit函数混入到beforeCreate中。

    vuexInit函数首先通过this.$options拿到传入的 options,他会判断是否有options.store,如果有则为this.$store挂载options.store,如果没有options.store,则会通过options.parent.$store拿到 store,并挂载到this.$store。也就是说,根部的 vue 实例,会首次对this.$store进行挂载,之后的子组件,则通过不断访问父组件的options.parent.$store进行挂载。

    // src/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.
        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) {
          this.$store =
            typeof options.store === "function" ? options.store() : options.store;
        } else if (options.parent && options.parent.$store) {
          this.$store = options.parent.$store;
        }
      }
    }
    

    通过 new Vuex.Store 进行 store 实例的创建

    class Store 实例化

    Store是定义在src/store.js下的一个 class,当我们执行 new 的操作,则会去执行其中的constructor函数。

    constructor函数主要做了

    • 初始化许多实例属性(其中最重要的为this._modules = new ModuleCollection(options)
    • 执行installModule函数
      • 初始化了根部module
      • 递归的注册了所有的子module
      • 把所有module的 getter 收集在了this._wrappedGetters
    • 执行了resetStoreVM函数,始化了 store 的 vm,并把 state,getter 变为了响应式的数据
    // src/store.js
    export class Store {
      constructor (options = {}) {
    
        ...
    
        // store internal state
        this._committing = false
        this._actions = Object.create(null)
        this._actionSubscribers = []
        this._mutations = Object.create(null)
        this._wrappedGetters = Object.create(null)
        this._modules = new ModuleCollection(options)
        this._modulesNamespaceMap = Object.create(null)
        this._subscribers = []
        this._watcherVM = new Vue()
        this._makeLocalGettersCache = Object.create(null)
    
        ...
    
        // init root module.
        // this also recursively registers all sub-modules
        // and collects all module getters inside this._wrappedGetters
        installModule(this, state, [], this._modules.root)
    
        // initialize the store vm, which is responsible for the reactivity
        // (also registers _wrappedGetters as computed properties)
        resetStoreVM(this, state)
        ...
      }
      ...
    }
    

    new ModuleCollection(options)

    class ModuleCollectionconstructor会执行this.register([], rawRootModule, false)(rawRootModules 为传入的 options)。

    register函数主要做了

    1. 通过new Module(rawModule, runtime)创建Module实例
    2. 如果判断传入的rawModulemodules,则遍历modules,继续调用自身(register函数)
    3. 通过path参数判断当前是否为根部 Module,如果是,则为 Module 实例的 root 属性绑定该 Module 实例,如果是子 Module,首先拿到父 Module,接着通过addChild方法建立父子关系,生成树状结构 Module
    // src/module/module-collection.js
    export default class ModuleCollection {
      constructor (rawRootModule) {
        // register root module (Vuex.Store options)
        this.register([], rawRootModule, false)
      }
    
      get (path) {
        // 首次调用由于是空数组,则返回this.root,也就是root Module,之后传入会根据传入的路径找到他的parent
        return path.reduce((module, key) => {
          // module._children[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) { // 首次进入path为空数组则满足条件
          this.root = newModule // 此时root为根部的store创建的Module实例
        }
        // 如果是在modules调用register函数,则传入的path不会为空数组,则会走到当前的逻辑
        else {
          // 首先调用this.get slice(0, -1) 排除掉最后一位,首次调用传入的是空数组,拿到this.root
          const parent = this.get(path.slice(0, -1))
          // 拿到parent之后通过调用addChild  => this._children[key] = module
          parent.addChild(path[path.length - 1], newModule)
        }
    
        // register nested modules
        if (rawModule.modules) { // 如果判断传入的结构有modules则执行
          // 遍历modules进行register函数的调用,rawChildModule为value,key为modules的item的具体名称
          forEachValue(rawModule.modules, (rawChildModule, key) => {
            // 当因为modules调用register的时候,传入的第一个参数不再一个空数组,而是会concat 当前modules key的数组
            // 而concat方法是不具有副作用的,这样就可以保证递归的调用register函数
            this.register(path.concat(key), rawChildModule, runtime)
          })
        }
      }
      ...
    }
    

    也就是说new ModuleCollection(options)主要是帮助我们生成了 module 实例,并形成了

    class module

    Module在实例化的过程中会执行constructor

    1. 通过this._rawModule = rawModule把传入的 store 结构保存
    2. 拿到当前传入constructor的结构,并把根部 state 作为实例属性 state 进行保存
    // src/store.js
    export default class Module {
      constructor (rawModule, runtime) {
        this.runtime = runtime // false
        // Store some children item
        this._children = Object.create(null)
        // Store the origin module object which passed by programmer
        this._rawModule = rawModule // 保存传入的store结构
        const rawState = rawModule.state // 根部state
    
        // Store the origin module's state
        this.state = (typeof rawState === 'function' ? rawState() : rawState) || {}
      }
      ...
    }
    

    installModule

    ModuleCollection实例赋值给this._modules后,下一步是安装 module。执行installModule(this, state, [], this._modules.root)传入了 4 个参数

    • 第二个参数state,为this._modules.root.state,也就是ModuleCollection实例的root.stateroot.state是根部的 Module 实例的 state。
    • 第四个参数this._modules.root则是根部的 Module 实例

    installModule函数主要分为三个分支

    1. 当前是根部 Module
    2. 当前是子 Module,并且 namespaced 为 true
    3. 当前是子 Module,并且 namespaced 不为 true

    installModule主要做了

    1. 首先定义了两个变量
    • isRoot 变量 => 通过第三个参数 path 数组的 length 来判断当前是否是根部 Module。
    • namespace 变量 => 通过store._modules.getNamespace(path)拿到对应 path 的 module 的访问路径(例如:modulesA/modulesB
    1. 通过makeLocalContext函数,拿到当前模块的 local(访问经过处理成完整路径的 mutation,action,state,getter) makeLocalContext(store, namespace, path)
    • 参数
      • 第一个参数传入 store 实例
      • 第二个参数传入拿到的namespace(访问路径)
      • 第三个参数传入路径数组 path
    • 执行
      • 定义 local 对象
      • 为 local 对象声明dispatchcommit。根据传入的namespace字段,决定当前环境(Module)的dispatchcommit在调用时的完整路径type,如果有namespacetype = namespace + type,这样当我们写 namespace 为 true 的 modules,则不需要在当前 Module 调用 commit 或 dispatch 时,书写完整路径
      • 通过Object.defineProperties为 local 对象声明gettersstate的访问方式
      • 返回 local 对象
    1. 通过registerMutationregisterActionregisterGetterinstallModule为当前 Module 实例,注册MutationActionGette以及子Module
    • 如果是installModule执行的是子 Module,则会通过store._withCommit去设置 store 的 state,形成树形结构
    // store, state, [], store._modules.root, true
    function installModule(store, rootState, path, module, hot) {
      const isRoot = !path.length; // 首次path为空数组,则为true
      // 根据path,获取当前的完整路径
      const namespace = store._modules.getNamespace(path);
    
      // register in namespace map
      if (module.namespaced) {
        if (store._modulesNamespaceMap[namespace] && __DEV__) {
          console.error(
            `[vuex] duplicate namespace ${namespace} for the namespaced module ${path.join(
              "/"
            )}`
          );
        }
        store._modulesNamespaceMap[namespace] = module;
      }
      // 假如当前是子模块
      // set state
      if (!isRoot && !hot) {
        // 拿到父级的state
        const parentState = getNestedState(rootState, path.slice(0, -1));
        // 拿到模块名称
        const moduleName = path[path.length - 1];
        store._withCommit(() => {
          if (__DEV__) {
            if (moduleName in parentState) {
              console.warn(
                `[vuex] state field "${moduleName}" was overridden by a module with the same name at "${path.join(
                  "."
                )}"`
              );
            }
          }
          // 去设置当前的state到state中
          Vue.set(parentState, moduleName, module.state);
        });
      }
      // 首先拿到当前模块的local(访问经过处理成完整路径的mutation,action,state,getter)
      const local = (module.context = makeLocalContext(store, namespace, path));
      // 为当前模块注册对应的Mutation
      module.forEachMutation((mutation, key) => {
        const namespacedType = namespace + key;
        registerMutation(store, namespacedType, mutation, local);
      });
      // 为当前模块注册对应的Action
      module.forEachAction((action, key) => {
        const type = action.root ? key : namespace + key;
        const handler = action.handler || action;
        registerAction(store, type, handler, local);
      });
      // 为当前模块注册对应的getter
      module.forEachGetter((getter, key) => {
        const namespacedType = namespace + key;
        registerGetter(store, namespacedType, getter, local);
      });
      // 遍历子module进行模块安装
      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 (__DEV__ && !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 (__DEV__ && !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;
    }
    

    resetStoreVM

    resetStoreVM

    1. 获取旧的store._vm,通过store._wrappedGetters拿到所有注册的 getters,定义空的computed对象
    2. 遍历store._wrappedGetters,拿到注册的 key,和对应的回调函数,注册在 computed 对象上,并通过Object.defineProperty进行数据劫持,定义 store 的实例getters属性最终会访问到store._vm[key]
    3. 为 store._vm 赋值new Vue,并且定义其中的 data 中的$$state属性为 state(store.state),computed 为上边定义的computed对象
    4. 如果有定义oldVm,则在nextTick(vue 当前队列结束)之后,销毁旧的 vm 实例。
    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) => {
        // 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.
        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
      ...
    
      Vue.config.silent = true
      store._vm = new Vue({
        data: {
          $$state: state
        },
        computed
      })
    
      ...
    
      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())
      }
    }
    

    Store 实例提供给我们使用的 api

    state,getters,commit,dispatch

    假设我们的结构是这样的

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

    state

    假如我们想访问 modulesA 中 state 的属性 count,官方文档给出的 api 是使用this.$store.state.modulesA.count

    当访问到state的时候会触发 Store 实例的state的 get 函数,他会返回this._vm._data.$$statestore._vm在执行resetStoreVM时,会通过new Vue进行初始化,data 中的$$state对应的是定义的 state。state 是resetStoreVM接受的第二个参数,也就是this._modules.root.state。在最初this._modules.root.state只代表根部 module 的 state,并没有形成一个嵌套的结构,state tree 的形成是在子模块执行installModule函数的时候,会通过Vue.set(parentState, moduleName, module.state),往 root.state 中,以模块的 key 作为 key,模块的 state 作为 value 进行添加。也就是说最终的$$state: state对应的 state 是一个树状结构的 state,这样当我们就可以通过state.modulesA.count拿到modulesA模块中的 count。如果我们对 state 进行直接修改,比如this.$store.state.moudlesA.count = 4,那么并不会成功的修改 state,因为state的 set 函数他并不会去做相应的 state 的修改,而是会在开发模式下报出一个警告。

    export class Store {
      ...
      get state () {
        return this._vm._data.$$state
      }
    
      set state (v) {
        if (__DEV__) {
          assert(false, `use store.replaceState() to explicit replace store state.`)
        }
      }
      ...
    }
    
    // src/store.js
    function installModule (store, rootState, path, module, hot) {
      ...
      // set state
      if (!isRoot && !hot) {
        // 拿到父级的state
        const parentState = getNestedState(rootState, path.slice(0, -1))
        // 拿到模块名称
        const moduleName = path[path.length - 1]
        store._withCommit(() => {
          ...
          // 去设置当前的state到父级state中
          Vue.set(parentState, moduleName, module.state)
        })
      }
      ...
    }
    

    getters

    假如我们想访问modulesA的getters中的computedConut,官方文档给出的api是使用this.$store.getters['modulesA/computedConut']

    getter的初始化同样是在resetStoreVM函数中,首先会定义一个空的computed对象,然后遍历store._wrappedGetters(在执行installModule函数的时候会执行registerGetter函数,在store._wrappedGetters挂载我们定义的getter函数),把getters函数作为值,getter函数的key作为computed的key,注册在computed,在遍历的时候还会通过Object.defineProperty定义了在访问store.getters.xxx的访问,最终会访问到store._vm[key],也就是会访问到store的vm实例上,为什么这么做,是因为接着,他会把computed作为vm实例的computed,这样通过访问this.$store.getters.xxx就会被代理到了store._vm[key]也就是我们经过vue实例computed处理过的具体的getters函数。

    // src/store.js
    function resetStoreVM (store, state, hot) {
      ...
      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.
        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
      ...
      store._vm = new Vue({
        data: {
          $$state: state
        },
        computed
      })
      ...
    }
    

    commit

    假如我们想调用modulesA的mutations中的add函数,来修改我们的state,那么需要执行this.$store.commit('modulesA/add')。store中的commit函数首先会通过unifyObjectStyle函数解析我们传入的三个参数,也就是文档中支持两种传入方式

    最终拿到三个参数,第一个是type也就是我们需要调用的mutations的名称,第二个参数是,payload,也就是我们需要传入所调用mutations的参数,第三个参数是options(在actions中提交commit可以传入root:true,让context访问到根部module)。接着commit函数会通过this._mutations[type](在registerMutation函数中通过)拿到对应的mutations函数,然后在_withCommit函数的包裹下,遍历执行(因为mutations在注册中会注册为一个数组),并把payload作为参数传入。为什么用_withCommit函数进行包裹,_withCommit函数帮我们做了这样一件事他首先把全局的_committing置为了true,在执行完其中的函数,在把他置为false,在函数resetStoreVM的执行中,如果传的strict是true,则会执行enableStrictMode函数,enableStrictMode函数的目的是通过vm的 $watch方法对this._data.$$state进行了监听,当修改的时候,如果他判断_committing为false,则会报错。也就是说如果通过mutations进行了state的修改,那么是不会报错的,如果我们擅自进行了修改,则会报错。传入root为true可以调用到根部commit的原因是在installModule函数中执行makeLocalContext函数时,他定义了namespaced的module下的commit会接受第三个参数,如果第三个参数(options)中有root为true,那么在调用commit的时候,传入的type就是原始定义的type,而不是和namespace拼接之后的type。

    // src/store.js
      commit (_type, _payload, _options) {
        // check object-style commit
        const {
          type,
          payload,
          options
        } = unifyObjectStyle(_type, _payload, _options)
    
        const mutation = { type, payload }
        const entry = this._mutations[type]
    
        ...
    
        this._withCommit(() => {
          entry.forEach(function commitIterator (handler) {
            handler(payload)
          })
        })
        ...
      }
    
    // src/store.js
      _withCommit (fn) {
        const committing = this._committing
        this._committing = true
        fn()
        this._committing = committing
      }
    
    // src/store.js
    function enableStrictMode (store) {
      store._vm.$watch(function () { return this._data.$$state }, () => {
        if (__DEV__) {
          assert(store._committing, `do not mutate vuex store state outside mutation handlers.`)
        }
      }, { deep: true, sync: true })
    }
    

    dispatch

    假如我们想调用modulesA的actions中的addCount函数,来提交提个mutation函数,那么需要执行this.$store.dispatch('modulesA/addCount')

    store中的dispatch函数和commit函数相似,首先通过unifyObjectStyle对传入的参数进行解析,拿到type和payload参数。同样也会去this._actions[type]中拿到对应的actions中注册的函数。和commit不同的是,他并不会直接执行,而是会先判断判断拿到的_actions[type]的length,如果是1则会执行,如果不是1,则会执行Promise.all(entry.map(handler => handler(payload))),这是因为actions在注册的时候会通过registerAction函数进行注册,registerAction函数中会判断传入的actions是否是一个promise如果不是promise,则会通过res = Promise.resolve(res),把他变成一个promise,dispatch函数最终会返回一个promise,其中的reslove的执行时机,正是执行entrypromise的then的时候。也就是说,我们调用dispatch最终会返回一个promise,这个promise触发then的时机,是对应的所有actions执行完的时候。其实dispatch是一个异步的函数,他可以接受一些异步的方法,最终提交mutation来修改state,dispatch很好的帮助我们规范了提交mutations的方式。

    // src/store.js
     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)
          })
        })
      }
    
    // src/store.js
    function registerAction (store, type, handler, local) {
      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) {
          ...
        } else {
          return res
        }
      })
    }
    

    registerModule和unregisterModule

    vuex还提供了动态对module的注册和注销

    • 注册模块
     this.$store.registerModule('c', {
          namespaced: true,
          ...
        })
     this.$store.registerModule('c', {
      namespaced: true,
      ...
    })
    

    registerModule函数首先对拿到的的第一个参数path做一个类型判断,如果他是string类型,则会执行path = [path]把他变成一个数组。接着会判断path是否是一个数组,如果不是一个数组,则报错这是因为registerModule接下来的第一步是通过this._modules.register,对module进行注册,而ModuleCollection实例,接受的path是一个数组类型的。接着会判断path.length > 0,如果我们第一个参数传入了一个空的字符串或者空数组,那么在执行register的时候,代表他是一个根部module,而registerModule函数不允许注册根部的module。判断了前置条件,接着会调用this._modules.register(path, rawModule)this._modulesModuleCollection实例,其中的register方法是用来注册Module实例的。执行完这一步,module实例创建完毕,接着会通过installModule安装模块,最终调用resetStoreVM函数,对store._vm进行重新的注册(data,computed),最后对旧的store._vm通过$destroy进行销毁。

    // src/store.js
      registerModule (path, rawModule, options = {}) {
        if (typeof path === 'string') path = [path]
    
        if (__DEV__) {
          assert(Array.isArray(path), `module path must be a string or an Array.`)
          assert(path.length > 0, 'cannot register the root module by using registerModule.')
        }
    
        this._modules.register(path, rawModule)
        installModule(this, this.state, path, this._modules.get(path), options.preserveState)
        // reset store to update getters...
        resetStoreVM(this, this.state)
      }
    
    • 注销模块
    this.$store.unregisterModule('c')
    this.$store.unregisterModule(['modulesA','c'])
    

    unregisterModule函数和registerModule函数相同,也会首先判断传入的path的类型,如果是string,则会执行path = [path],接着判断如果不是数组则报错。接着调用this._modules.unregister(path)ModuleCollection实例的unregister函数,会首先通过传入的paththis.get(path.slice(0, -1))拿到父module,然后通过path拿到传入的keyconst key = path[path.length - 1],拿到key和parent,则执行const child = parent.getChild(key),module实例的getChild函数,实际是获取了当前module的_children属性,_children属性是在执行ModuleCollection实例的register函数的时候,通过module实例的addChild进行添加。如果通过key找到了对用的module实例,则会parent.removeChild(key),也就是module实例的delete this._children[key],通过delete删除对应的父子依赖关系。接着unregisterModule会通过this._withCommit去修改vm实例的data,首先通过getNestedState(this.state, path.slice(0, -1))找到对应的父state,然后调用Vue.delete(parentState, path[path.length - 1]),删除vm实例中对应的模块的state。删除之后执行resetStoreresetStore函数把store实例的_actions,_mutations,_wrappedGetters_modulesNamespaceMap都重置为了空,重新通过store实例拿到state,通过调用installModuleresetStoreVM函数进行重新安装和重新注册vm实例。

    // src/store.js
      unregisterModule (path) {
        if (typeof path === 'string') path = [path]
    
        if (__DEV__) {
          assert(Array.isArray(path), `module path must be a string or an Array.`)
        }
    
        this._modules.unregister(path)
        this._withCommit(() => {
          const parentState = getNestedState(this.state, path.slice(0, -1))
          Vue.delete(parentState, path[path.length - 1])
        })
        resetStore(this)
      }
      ...
    function resetStore (store, hot) {
      store._actions = Object.create(null)
      store._mutations = Object.create(null)
      store._wrappedGetters = Object.create(null)
      store._modulesNamespaceMap = Object.create(null)
      const state = store.state
      // init all modules
      installModule(store, state, [], store._modules.root, true)
      // reset vm
      resetStoreVM(store, state, hot)
    }
    

    辅助函数

    mapState

    在调用的时候,我们可以通过往mapState函数中传递数组或对象获取相应的state。

    参数为对象

     computed: mapState({
        count: state => state.count,
        countAlias: 'count',
      })
    

    参数为数组

    computed: mapState([
      // 映射 this.count 为 store.state.count
      'count'
    ])
    

    mapState函数定义在src文件夹下的helpers.js文件中,首先最外层包裹了normalizeNamespace函数,normalizeNamespace函数他会对拿到的参数进行解析,如果我们只传递了一个参数(数组或对象),那么在执行typeof namespace !== 'string'的时候会执行其中的逻辑map = namespace;namespace = '',这样在真正执行mapState内部的函数,参数传递就进行了统一处理。如果我们传递了两个参数,那么会判断我们传递的第一个参数namespace是否结尾是/,如果不是,会为我们在末尾拼接/,最终把处理过后的namespacemap两个参数传入内部的函数去执行。mapState内部的函数首先定义一个空的res变量,接着把第二个参数states作为参数,传入normalizeMap函数。normalizeMap函数的作用是把我们传入的数或者对象统一处理为一个数组对象,这个数组对象中key属性对应的value就是我们传入的key(如果是数组,直接把string作为key),然后把value作为数组对象的val。接着对这个数组对象进行遍历,把每一项的key作为res的key,value是定义的mappedState函数。最终把res对象return出去。也就是说最终当我们执行mapState辅助函数,那么最终拿到的是一个res,他的key是我们传入的key,value是mappedState函数,如果我们把他放入computed中,由于computed在访问时,会自动求值,会把我们的value,mappedState函数作为computed的getter执行。mappedState函数首先会通过this.$store.statethis.$store.getters拿到全局的state和getters,然后会判断namespace是否为空,如果我们传入了这个字段(代表我们希望获取到modules中的state),那么他首先会把store实例,'mapState'字符串和传入的namespace作为参数调用getModuleByNamespace函数。getModuleByNamespace函数通过传入的namespace,去访问store._modulesNamespaceMap[namespace]store._modulesNamespaceMapinstallModule函数中判断当前模块是否有namesapced,如果有,会把当前拿到的module注册在store._modulesNamespaceMap下。也就是说最终getModuleByNamespace会根据namespace拿到对应的module。拿到对应的module,mappedState函数接着会通过module.context拿到对应的state和getters。module.context是在installModule函数中,拿到makeLocalContext函数的结果进行注册的。其中的getter和state是经过处理后的,访问getter其实是最终store.getters[type]中的getter,state则为store._vm.state。拿到state和getters最后会判断我们传入的val,如果val是函数,则执行这个函数,并且把state和getters作为两个参数传入,否则返回state[val],所以当我们如果写的value是一个函数,那么第一个参数就是当前namespace对应的module的state。

    // src/helpers.js
    /**
     * Reduce the code which written in Vue.js for getting the state.
     * @param {String} [namespace] - Module's namespace
     * @param {Object|Array} states # Object's item can be a function which accept state and getters for param, you can do something for state and getters in it.
     * @param {Object}
     */
    export const mapState = normalizeNamespace((namespace, states) => {
      const res = {}
      ...
      normalizeMap(states).forEach(({ key, val }) => {
        res[key] = function mappedState () {
          let state = this.$store.state
          let getters = this.$store.getters
          if (namespace) {
            const module = getModuleByNamespace(this.$store, 'mapState', namespace)
            if (!module) {
              return
            }
            state = module.context.state
            getters = module.context.getters
          }
          return typeof val === 'function'
            ? val.call(this, state, getters)
            : state[val]
        }
        ...
      })
      return res
    })
    ...
    /**
     * Normalize the map
     * normalizeMap([1, 2, 3]) => [ { key: 1, val: 1 }, { key: 2, val: 2 }, { key: 3, val: 3 } ]
     * normalizeMap({a: 1, b: 2, c: 3}) => [ { key: 'a', val: 1 }, { key: 'b', val: 2 }, { key: 'c', val: 3 } ]
     * @param {Array|Object} map
     * @return {Object}
     */
    function normalizeMap (map) {
      if (!isValidMap(map)) {
        return []
      }
      return Array.isArray(map)
        ? map.map(key => ({ key, val: key }))
        : Object.keys(map).map(key => ({ key, val: map[key] }))
    }
    ...
    /**
     * Search a special module from store by namespace. if module not exist, print error message.
     * @param {Object} store
     * @param {String} helper
     * @param {String} namespace
     * @return {Object}
     */
    function getModuleByNamespace (store, helper, namespace) {
      const module = store._modulesNamespaceMap[namespace]
      if (__DEV__ && !module) {
        console.error(`[vuex] module namespace not found in ${helper}(): ${namespace}`)
      }
      return module
    }
    
    

    mapGetters

    在调用的时候,我们可以通过往mapGetters函数中传递数组或对象获取相应的getters。参数为对象

    computed: mapState('modulesA/', {
      computedcountA: 'computedcount'
    })
    

    参数为数组

    computed: mapState('modulesA/', ['computedcount'])
    

    mapGetters同样定义在helper文件中,和mapState相同,他首先通过normalizeNamespace函数进行包裹(对传入的参数进行统一的处理,最终处理为namespace和map)。接着他也会定义一个res对象,用来获取存储的getters对象 ,并最终返回。通过normalizeMap函数对传入的map进行统一的处理成一个数组对象后,进行遍历。并把key作为res的key,value为mappedGetter函数。当我们访问getters的时候,会去执行mappedGetter函数,mappedGetter函数中的处理和mappedState函数有所不同,他首先会判断namespace,如果有的话会接着通过getModuleByNamespace函数判断是否可以通过传入的namespace找到对应的module,如果没有对应的module则会结束函数的执行,也是说,如果我们传入了namespace但是却没有找到对应的module,则会结束函数的执行。getModuleByNamespace函数会在dev模式下,如果没有找到对应的module,会报出这样的警告console.error(`[vuex] module namespace not found in ${helper}(): ${namespace}`)当我们在开发的过程当中遇到了这样的报错,就说明我们传入的namespace可能是有问题的,vuex并没有通过这个namespace找到对应的modle。mappedGetter函数最终会返回this.$store.getters[val]store.getters在执行resetStoreVM函数的时候,在遍历store._wrappedGetters的时候,通过Object.defineProperty进行注册的,他的getter为() => store._vm[key],也就是store的vm实例的computed。store._wrappedGetters是在执行installModule中的registerGetter函数的时候,进行绑定的。

    // src/helpers.js
    /**
     * Reduce the code which written in Vue.js for getting the getters
     * @param {String} [namespace] - Module's namespace
     * @param {Object|Array} getters
     * @return {Object}
     */
    export const mapGetters = normalizeNamespace((namespace, getters) => {
      const res = {}
      ...
      normalizeMap(getters).forEach(({ key, val }) => {
        // The namespace has been mutated by normalizeNamespace
        val = namespace + val
        res[key] = function mappedGetter () {
          if (namespace && !getModuleByNamespace(this.$store, 'mapGetters', namespace)) {
            return
          }
          ...
          return this.$store.getters[val]
        }
        ...
      })
      return res
    })
    

    mapMutations

    在调用的时候,我们可以通过往mapMutations函数中传递数组或对象获取相应的mutations。 参数为对象

     methods: {
        ...mapMutations('modulesA', {
          add(commit, ...rest) {
            commit('add', ...rest)
          },
          addCount:'add'
        })
      },
    

    参数为数组

     methods: {
        ...mapMutations('modulesA', ['add'])
      },
    

    在调用的时候,我们可以通调用mapMutations,生成组件的mutationn方法,第一个参数和mapState和mapGetters相同都是可选的传入namespace,第二个参数可以是一个数组或者对象,对象的值可以是一个string,也可以是一个function。mapMutations函数也是通过normalizeNamespace进行一层传入的参数的处理。处理之后首先也会定义一个对象res,然后通过normalizeMap函数对传入的map进行处理。之后遍历处理后的map,以mutations的key作为res的key,mappedMutation函数,作为value,最终返回res。这块的处理和之前的mapState和mapGetters是相同的。当我们调用对用的mutations的时候,会去执行mappedMutation函数,mappedMutation函数首先会通过=this.$store.commit拿到store实例的commit方法,然后会判断是否传入了namespoace,如果传入了namespoace,同样也会通过getModuleByNamespace函数拿到对应的module,然后重新定义当前的commit为module.context.commit,也就是说如果没有传入namespace(全局mutations)则会使用全局的commit方法,如果传入了namespace,则会去找到对应module的局部commit方法。module.context.commit在执行installModule函数中的makeLocalContext进行了定义,他会判断当前是否有namespace,如果有的话他会重新定义在执行store.commit的时候,传入的第一个参数type为拼接了namespace的结果。最后会判断拿到val是否是一个function,如果是一个function,则会去执行这个函数,并且把拿到的commit(局部或全局commit),和传入的剩余参数作为调用传入的这个函数的参数。也就是说如果我们写的是一个函数,那么函数的第一个参数将会是commit。如果判断不是一个函数,则会执行拿到的commit,并把传入的mutations名称作为第一个参数,传入的其他参数也传入。这样通过局部的commit再去找到完整的当前module的mutations的函数,最终调用。

    // src/helpers.js
    /**
     * Reduce the code which written in Vue.js for committing the mutation
     * @param {String} [namespace] - Module's namespace
     * @param {Object|Array} mutations # Object's item can be a function which accept `commit` function as the first param, it can accept another params. You can commit mutation and do any other things in this function. specially, You need to pass anthor params from the mapped function.
     * @return {Object}
     */
    export const mapMutations = normalizeNamespace((namespace, mutations) => {
      const res = {}
      ...
      normalizeMap(mutations).forEach(({ key, val }) => {
        res[key] = function mappedMutation (...args) {
          // Get the commit method from store
          let commit = this.$store.commit
          if (namespace) {
            const module = getModuleByNamespace(this.$store, 'mapMutations', namespace)
            if (!module) {
              return
            }
            commit = module.context.commit
          }
          return typeof val === 'function'
            ? val.apply(this, [commit].concat(args))
            : commit.apply(this.$store, [val].concat(args))
        }
      })
      return res
    })
    

    mapActions

    在调用的时候,我们可以通过往mapActions函数中传递数组或对象获取相应的actions。 参数为对象

     methods: {
        ...mapActions('modulesA', {
          addCount:'addCount',
          async addCount(dispatch) {
            await dispatch('addCount')
          }
        })
    

    参数为数组

     methods: {
         ...mapActions('modulesA', ['addCount'])
      },
    

    mapActions函数和mapMutations函数几乎是相同的,只是把commit换成了dispatch,这里就不在赘述他的实现步骤了。

    createNamespacedHelpers

    对于组件绑定的上述4个方法,的确帮助我们节省了部分的代码,但是如果还是面临一个问题,如果我们当前的组件需要操作同一个namespace下的state,action,mutation,getter,则需要传入4次namespace,所以createNamespacedHelpers函数正是为了解决这个问题而存在的。createNamespacedHelpers函数接受一分为参数namespace,然后向外暴露了一个对象,这个对象中有4个属性,分别代表mapState,mapGetters,mapMutations,mapActionsmapState: mapState.bind(null, namespace),他巧妙的利用了bind函数的特性,把传入的namespace作为第一个参数,之后当我们再去写参数的时候,就是第二个参数了,这样第一个参数namespace就被提前定义了。

    // src/helpers.js
    /**
     * Rebinding namespace param for mapXXX function in special scoped, and return them by simple object
     * @param {String} namespace
     * @return {Object}
     */
    export const createNamespacedHelpers = (namespace) => ({
      mapState: mapState.bind(null, namespace),
      mapGetters: mapGetters.bind(null, namespace),
      mapMutations: mapMutations.bind(null, namespace),
      mapActions: mapActions.bind(null, namespace)
    })
    

    起源地下载网 » Vuex 源码浅析(概览)

    常见问题FAQ

    免费下载或者VIP会员专享资源能否直接商用?
    本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
    提示下载完但解压或打开不了?
    最常见的情况是下载不完整: 可对比下载完压缩包的与网盘上的容量,若小于网盘提示的容量则是这个原因。这是浏览器下载的bug,建议用百度网盘软件或迅雷下载。若排除这种情况,可在对应资源底部留言,或 联络我们.。
    找不到素材资源介绍文章里的示例图片?
    对于PPT,KEY,Mockups,APP,网页模版等类型的素材,文章内用于介绍的图片通常并不包含在对应可供下载素材包内。这些相关商业图片需另外购买,且本站不负责(也没有办法)找到出处。 同样地一些字体文件也是这种情况,但部分素材会在素材包内有一份字体下载链接清单。
    模板不会安装或需要功能定制以及二次开发?
    请QQ联系我们

    发表评论

    还没有评论,快来抢沙发吧!

    如需帝国cms功能定制以及二次开发请联系我们

    联系作者

    请选择支付方式

    ×
    迅虎支付宝
    迅虎微信
    支付宝当面付
    余额支付
    ×
    微信扫码支付 0 元