Vuex-API源码字典

218 阅读9分钟

Vuex.Store

  • 用法
import Vue from 'vue'
import Vuex from 'vuex'

Vue.use(Vuex)

const store = new Vuex.Store({ ...options })

new Vue({
  el: '#app',
  store: store,
})
  • 核心源码
// Vue.use入口
function install(_Vue) {
  if (Vue && _Vue === Vue) {
    {
      console.error(
        '[vuex] already installed. Vue.use(Vuex) should be called only once.'
      );
    }
    return
  }
  Vue = _Vue;
  applyMixin(Vue);
}

/**
 * 初始化混入
 * @param {Object} Vue Vue
 */
function applyMixin(Vue) {
  var version = Number(Vue.version.split('.')[0]);

  if (version >= 2) {
    // 大于或等于2版本,直接使用Vue.mixin进行混入
    // beforeCreate生命周期等于: vuexInit函数
    Vue.mixin({ beforeCreate: vuexInit });
  } else {
    // 重写Vue原型上的_init初始化函数
    var _init = Vue.prototype._init;
    Vue.prototype._init = function (options) {
      if ( options === void 0 ) options = {};

      options.init = options.init
        ? [vuexInit].concat(options.init) // new Vue传入的options有init时,格式化options.init = [vuexInit, options.init]
      : vuexInit; // 否则,options.init = vuexInit
      _init.call(this, options);
    };
  }

  /**
   * Vuex初始化函数
   */
  function vuexInit() {
    // this.$options = Vue.prototype.$options = new Vue传入的对象
    var options = this.$options;

    // options.store存在时,this.$store = options.store() || options.store
    // options.parent & options.parent.$store存在时,this.$store = options.parent.$store
    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;
    }
  }
}

Vuex.Store构造器选项

state

Vuex store 实例的根 state 对象。和Vue的data类似。

  • 使用方式
const store = new Vuex.Store({
  state: {
    name: 'fanqiewa'
  }
})

const vm = new Vue({
  // ...
  computed: {
    name() {
      return this.$store.state.name
    }
  }
})
  • 核心源码
var Store = function Store(options) {
  // ...
  // 注册根module(注册了state、getters、mutations、actions)
  installModule(this, state, [], this._modules.root);
  // ...
}

function installModule(store, rootState, path, module, hot) {
  // 是否是根模块
  var isRoot = !path.length;
  // ...
  if (!isRoot && !hot) {
    var parentState = getNestedState(rootState, paht.slice(0, -1));
    var moduleName = path[path.length - 1];
    store._withCommit(function () {
      {
        if (moduleName in parentState) {
          console.warn(
          	("[vuex] state field \"" + moduleName + "\" was overridden by a module with the same name at \"" + (path.join('.')) + "\"")
          );
        }
      }
      // 给父级state添加属性,属性名为子模块module名,值为子模块的state 
      Vue.set(parentState, moduleName, module.state);
    })
  }
  // ...
}

var prototypeAccessors$1 = { state: { configurable: true } };

prototypeAccessors$1.state.get = function () {
  // this._vm 为Vue实例
  // _data 等于Vue实例的data
  return this._vm._data.$$state
};

prototypeAccessors$1.state.set = function (v) {
  {
     // 不能直接更改state对象
     assert(false, "use store.replaceState() to explicit replace store state.");
  }
};

// 设置state的get和set
Object.defineProperties( Store.prototype, prototypeAccessors$1 );

mutations

在 store 上注册 mutation,处理函数总是接收 state 作为第一个参数(如果定义在模块中,则为模块的局部状态),payload 作为第二个参数(可选)。

在大多数情况下,载荷应该是一个对象,这样可以包含多个字段并且记录的 mutation 会更易读。

  • 类型:{ [type: string]: Function }
  • 使用方式
const store = new Vuex.Store({
  state: {
    count: 1
  },
  mutations: {
    increment (state, n) {
      // 变更状态
      state.count += n
    }
  }
})

// 调用方式
store.commit('increment', 10)
  • 核心源码
function installModule(store, rootState, path, module, hot) {
  // ...
  // 遍历mutations,执行回调
  module.forEachMutation(function (mutation, key) {
    var namespacedType = namespace + key;
    registerMutation(store, namespacedType, mutation, local);
  });
  // ...
}

// 注册程序员定义的mutations
function registerMutation(store, type, handler, local) {
  var entry = store._mutations[type] || (store._mutations[type] = []);
  entry.push(function wrappedMutationHandler(payload) {
    handler.call(store, local.state, payload);
  });
}

// 程序员调用的commit
Store.prototype.commit = function commit(_type, _payload, _options) {
	var this$1 = this;
  
  // 统一对象形式
  var ref = unifyObjectStyle(_type, _payload, _options);
  
  var type = ref.type;
  var payload = ref.payload;
  var options = ref.options;
  
  var mutation = { type: type, payload: payload };
  
  // 在new Store时注册的mutations
  var entry = this._mutations[type];
  if (!entry) {
    {
      console.error(("[vuex] unknown mutation type: " + type));
    }
    return
  }
  // 通过_withCommit方式来触发mutations
  this._withCommit(function () {
    entry.forEach(function commitIterator(handler) {
      handler(payload);
    });
  });
  
  // 取出所有已注册的回调并执行(注册方式:store.subscribe(fn))
  // 在mutations执行之后触发
  this._subscribers
    .slice() // 如果订阅服务器同步调用unsubscribe,则浅拷贝可防止迭代器失效 unsubscribe
    .forEach(function (sub) { return sub(mutation, this$1.state); });
  
  if (
      options && options.silent
  ) {
    // 载荷中的silent属性已经删除了
    // 取消 Vue 所有的日志与警告
    console.warn(
      "[vuex] mutation type: " + type + ". Silent option has been removed. " +
      'Use the filter functionality in the vue-devtools'
    );
  }
}

action

在 store 上注册 action。处理函数总是接受 context 作为第一个参数,payload 作为第二个参数(可选)。

context 对象包含以下属性:

{
  state,      // 等同于 `store.state`,若在模块中则为局部状态
  rootState,  // 等同于 `store.state`,只存在于模块中
  commit,     // 等同于 `store.commit`
  dispatch,   // 等同于 `store.dispatch`
  getters,    // 等同于 `store.getters`
  rootGetters // 等同于 `store.getters`,只存在于模块中
}
  • 类型:{ [type: string]: Function }
  • 使用方式
const store = new Vuex.Store({
  state: {
    count: 0
  },
  mutations: {
    increment (state) {
      state.count++
    }
  },
  action: {
    increment (context) {
      context.commit('increment')
    }
  }
})

// 调用方式
store.dispatch('increment')
  • 组合action

action通常都是异步的。这是action和mutation最大的区别。如何体现的呢?

actions: {
  actionA ({ commit }) {
    return new Promise((resolve, reject) => {
      setTimeout(() => {
        commit('someMutation')
        resolve()
      }, 1000)
    })
  }
}

// 使用
store.dispatch('actionA').then(() => {
  // ...
})
  • 核心源码
function installModule(store, rootState, path, module, hot) {
  // ...
  // 遍历actions,执行回调
  module.forEachAction(function (mutation, key) {
    var type = action.root ? key : namespace + key;
    /**
        两种形式
        actions: {
          increment(context) { },
          decrement: {
            handler: function() { },
            // 扩展 表示当前action注册到根对象上
            root: true 
          }
        }
      */
    var handler = action.handler || action;
    registerAction(store, type, handler, local);
  });
  // ...
}

// 注册程序员定义的actions
function registerAction(store, type, handler, local) {
  var entry = store._actions[type] || (store._actions[type] = []);
  entry.push(function wrappedActionHandler (payload) {
    // modules的注意事项1在此可以体现出来,第一个参数为context,其中包含了6项,第二个参数为dispatch时传递的payload
    var 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(function (err) {
        store._devtoolHook.emit('vuex:error', err);
        throw err
      })
    } else {
      return res
    }
  });
}

// 程序员调用的dispatch
Store.prototype.dispatch = function dispatch(_type, _payload) {
  var this$1 = this;

  // 统一对象形式
  var ref = unifyObjectStyle(_type, _payload);
  var type = ref.type;
  var payload = ref.payload;

  var action = { type: type, payload: payload };
  // 在new Store时注册的actions
  var entry = this._actions[type];
  if (!entry) {
    {
      console.error(("[vuex] unknown action type: " + type));
    }
    return
  }

  try {
    // 取出所有已注册的回调并执行(注册方式:store.subscribeAction(fn,options))
    // 在actions执行之前触发
    this._actionSubscribers
      .slice() 
      .filter(function (sub) { return sub.before; })
      .forEach(function (sub) { return sub.before(action, this$1.state); });
  } catch (e) {
    {
      console.warn("[vuex] error in before action subscribers: ");
      console.error(e);
    }
  }

  // 确保所有promise对象都执行完
  var result = entry.length > 1
  ? Promise.all(entry.map(function (handler) { return handler(payload); }))
  : entry[0](payload);

  // 返回一个promise对象
  return new Promise(function (resolve, reject) {
    result.then(function (res) {
      try {
        // 取出所有已注册的回调并执行(注册方式:store.subscribeAction(fn,options))
        // 在actions执行之后触发
        this$1._actionSubscribers
          .filter(function (sub) { return sub.after; })
          .forEach(function (sub) { return sub.after(action, this$1.state); });
      } catch (e) {
        {
          console.warn("[vuex] error in after action subscribers: ");
          console.error(e);
        }
      }
      resolve(res);
    }, function (error) {
      try {
        this$1._actionSubscribers
          .filter(function (sub) { return sub.error; })
          .forEach(function (sub) { return sub.error(action, this$1.state, error); });
      } catch (e) {
        {
          console.warn("[vuex] error in error action subscribers: ");
          console.error(e);
        }
      }
      reject(error);
    });
  })
}

getters

在store上注册getter,getter方法接受以下参数

state, // 如果在模块中定义则为模块的局部状态
getters, // 等同于store.getters

当定义在一个模块里时会特别一些:

state, // 如果在模块中定义则为模块的局部状态
getters, // 等同于store.getters
rootState, // 等同于store.state
rootGetters, // 所有getters

注册的getter暴露为store.getters

  • 类型:{ [key: string]: Function }
  • 使用方式
const store = new Vuex.Store({
  state: {
    todos: [
      { id: 1, text: '...', done: true },
      { id: 2, text: '...', done: false }
    ]
  },
  getters: {
    doneTodos: (state, getters) => {
      return state.todos.filter(todo => todo.done)
    }
  }
})

// 调用方式
store.getters.doneTodos // => [{ id: 1, text: '...', done: true }]
  • 核心源码
function installModule(store, rootState, path, module, hot) {
  // ...
 	// 遍历getters,执行回调
  module.forEachGetter(function (getter, key) {
    var namespacedType = namespace + key;
    registerGetter(store, namespacedType, getter, local);
  });
  // ...
}

// 注册程序员定义的getters
function registerGetter (store, type, rawGetter, local) {
  if (store._wrappedGetters[type]) {
    {
      console.error(("[vuex] duplicate getter key: " + type));
    }
    return
  }
  store._wrappedGetters[type] = function wrappedGetter (store) {
    // modules的注意事项2在此可以体现出来,第一个参数为当前模块的state,第二个参数为当前模块的getters,第三个参数为根节点的state,第四个参数为根节点的getters
    return rawGetter(
      local.state, // local state
      local.getters, // local getters
      store.state, // root state
      store.getters // root getters
    )
  };
}
var Store = function Store (options) {
  // ...
	// 重置storeVM(这里属于初始化)
  resetStoreVM(this, state);
}
/**
 * 重置store实例
 * @param {Object} store Store实例
 * @param {Object} state store.state
 * @param {*} hot 
 */
function resetStoreVM (store, state, hot) {
  var oldVm = store._vm;

  // 绑定store公共的getters,使用getters时,从这里取值
  store.getters = {};
  // 重置本地缓存的getters对象
  store._makeLocalGettersCache = Object.create(null);
  // 已注册的 getters
  var wrappedGetters = store._wrappedGetters;
  // 定义一个计算属性对象
  var computed = {};
  forEachValue(wrappedGetters, function (fn, key) {
    // 每一个计算属性都是一个单独的闭包函数
    computed[key] = partial(fn, store);
    Object.defineProperty(store.getters, key, {
      // 调用getters时,实际上调用的是Vue实例的computed属性 ---------- important-api-getter 的使用
      get: function () { return store._vm[key]; },
      enumerable: true
    });
  });

  var silent = Vue.config.silent;
  // 取消警告
  Vue.config.silent = true;
  store._vm = new Vue({
    data: {
      $$state: state // 利用$$state来存储state
    },
    computed: computed
  });
	// ...
}

modules

由于使用单一状态树,应用的所有状态会集中到一个比较大的对象。当应用变得非常复杂时,store对象就有可能变得相当臃肿。

为了解决以上问题,Vuex允许我们将store分割成模块(module)。每个模块拥有自己的state、mutation、action、getter、甚至是嵌套子模块——从上至下进行同样方式的分割

  • 类型:Object
const moduleA = {
  state: () => ({ ... }),
  mutations: { ... },
  actions: { ... },
  getters: { ... }
}

const moduleB = {
  state: () => ({ ... }),
  mutations: { ... },
  actions: { ... }
}

const store = new Vuex.Store({
  modules: {
    a: moduleA,
    b: moduleB
  }
})

store.state.a // -> moduleA 的状态
store.state.b // -> moduleB 的状态

模块的局部状态

const moduleA = {
  state: () => ({
    count: 0
  }),
  mutations: {
    increment (state) {
      // 这里的 `state` 对象是模块的局部状态
      state.count++
    }
  },

  getters: {
    doubleCount (state) {
      return state.count * 2
    }
  }
}

注意1:同样,对于模块内部的action,局部状态通过context.state暴露出来,根节点状态则为context.rootState

const moduleA = {
  // ...
  actions: {
    incrementIfOddOnRootSum ({ state, commit, rootState }) {
      if ((state.count + rootState.count) % 2 === 1) {
        commit('increment')
      }
    }
  }
}

注意2:对于模块内部的getter,根节点状态会作为第三个参数暴露出来:

const moduleA = {
  // ...
  getters: {
    sumWithRootCount (state, getters, rootState) {
      return state.count + rootState.count
    }
  }
}
  • 核心源码
 // modules集合的构造器
var ModuleCollection = function ModuleCollection (rawRootModule) {
  // 注册根modules
  this.register([], rawRootModule, false);
};
/** 4、注册module */
ModuleCollection.prototype.register = function register (path, rawModule, runtime) {
  var this$1 = this;
	// ...
  var newModule = new Module(rawModule, runtime);
  if (path.length === 0) {
    this.root = newModule; // 根module
  } else {
    // path.slice(0, -1)获取除了数组最后一位的所有项
    // ModuleCollection.prototype.get返回指定命名空间下的module,path为空时,返回this.root
    var parent = this.get(path.slice(0, -1));
    // 将path数组中的最后一位(也就是自己),绑定到parent的_children对象上
    parent.addChild(path[path.length - 1], newModule);
  }

  // 程序员传递的options有modules时
  if (rawModule.modules) {
    forEachValue(rawModule.modules, function (rawChildModule, key) {
      // 注册子模块,path数组添加modules的属性名
      this$1.register(path.concat(key), rawChildModule, runtime);
    });
  }
};
var Store = function Store (options) {
  // 存储modules
  this._modules = new ModuleCollection(options);
  // ...
};
function installModule(store, rootState, path, module, hot) {
  // ...
 
  // 遍历子模块(如果有的话,在new ModuleCollection时,已经往_child中添加了子模块,在这里是可以拿得到的),执行回调
  module.forEachChild(function (child, key) {
    // 安装子模块
    installModule(store, rootState, path.concat(key), child, hot);
  });
  // ...
}

命名空间

默认情况下,模块内部的 action、mutation 和 getter 是注册在全局命名空间的——这样使得多个模块能够对同一 mutation 或 action 作出响应。

如果希望你的模块具有更高的封装度和复用性,你可以通过添加 namespaced: true 的方式使其成为带命名空间的模块。当模块被注册后,它的所有 getter、action 及 mutation 都会自动根据模块注册的路径调整命名。例如:

const store = new Vuex.Store({
  modules: {
    account: {
      namespaced: true,

      // 模块内容(module assets)
      state: () => ({ ... }), // 模块内的状态已经是嵌套的了,使用 `namespaced` 属性不会对其产生影响
      getters: {
        isAdmin () { ... } // -> getters['account/isAdmin']
      },
      actions: {
        login () { ... } // -> dispatch('account/login')
      },
      mutations: {
        login () { ... } // -> commit('account/login')
      },

      // 嵌套模块
      modules: {
        // 继承父模块的命名空间
        myPage: {
          state: () => ({ ... }),
          getters: {
            profile () { ... } // -> getters['account/profile']
          }
        },

        // 进一步嵌套命名空间
        posts: {
          namespaced: true,

          state: () => ({ ... }),
          getters: {
            popular () { ... } // -> getters['account/posts/popular']
          }
        }
      }
    }
  }
})
  • 类型:Boolean

启用了命名空间的 getter 和 action 会收到局部化的 getterdispatchcommit。换言之,你在使用模块内容(module assets)时不需要在同一模块内额外添加空间名前缀。更改 namespaced 属性后不需要修改模块内的代码。

注意1:在带命名空间的模块内访问全局内容(Global Assets)

如果你希望使用全局 state 和 getter,rootStaterootGetters 会作为第三和第四参数传入 getter,也会通过 context 对象的属性传入 action。

若需要在全局命名空间内分发 action 或提交 mutation,将 { root: true } 作为第三参数传给 dispatchcommit 即可。

modules: {
  foo: {
    namespaced: true,

    getters: {
      // 在这个模块的 getter 中,`getters` 被局部化了
      // 你可以使用 getter 的第四个参数来调用 `rootGetters`
      someGetter (state, getters, rootState, rootGetters) {
        getters.someOtherGetter // -> 'foo/someOtherGetter'
        rootGetters.someOtherGetter // -> 'someOtherGetter'
      },
      someOtherGetter: state => { ... }
    },

    actions: {
      // 在这个模块中, dispatch 和 commit 也被局部化了
      // 他们可以接受 `root` 属性以访问根 dispatch 或 commit
      someAction ({ dispatch, commit, getters, rootGetters }) {
        getters.someGetter // -> 'foo/someGetter'
        rootGetters.someGetter // -> 'someGetter'

        dispatch('someOtherAction') // -> 'foo/someOtherAction'
        dispatch('someOtherAction', null, { root: true }) // -> 'someOtherAction'

        commit('someMutation') // -> 'foo/someMutation'
        commit('someMutation', null, { root: true }) // -> 'someMutation'
      },
      someOtherAction (ctx, payload) { ... }
    }
  }
}
  • 核心源码
 /**
   * 局部化dispatch, commit, getters and state
   * 也就是在模块里定义的actions函数,在函数内部,可以使用local定义的dispatch, commit, getters and state
   */
  function makeLocalContext (store, namespace, path) {
    var noNamespace = namespace === '';

    var local = {
      dispatch: noNamespace ? store.dispatch : function (_type, _payload, _options) {

        // 有命名空间的模块,在注册actions时,局部可以调用这个dispatch方法
        var args = unifyObjectStyle(_type, _payload, _options);
        var payload = args.payload;
        var options = args.options;
        var type = args.type;

        /**
          不传第三个参数或第三个参数中的root为false时,将会调用根的actions
          eg: 
          modules: {
            foo: {
              namespaced: true,
              actions: {
                // 在这个模块中, dispatch 和 commit 也被局部化了
                // 他们可以接受 `root` 属性以访问根 dispatch 或 commit
                someAction ({ dispatch, commit, getters, rootGetters }) {
                  getters.someGetter // -> 'foo/someGetter'
                  rootGetters.someGetter // -> 'someGetter'

                  dispatch('someOtherAction') // -> 'foo/someOtherAction'
                  dispatch('someOtherAction', null, { root: true }) // -> 'someOtherAction'

                  commit('someMutation') // -> 'foo/someMutation'
                  commit('someMutation', null, { root: true }) // -> 'someMutation'
                },
                someOtherAction (ctx, payload) { ... }
              }
            }
          }
        */
        if (!options || !options.root) {
          type = namespace + type;
          if ( !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 : function (_type, _payload, _options) {
        var args = unifyObjectStyle(_type, _payload, _options);
        var payload = args.payload;
        var options = args.options;
        var type = args.type;

        if (!options || !options.root) {
          type = namespace + type;
          if ( !store._mutations[type]) {
            console.error(("[vuex] unknown local mutation type: " + (args.type) + ", global type: " + type));
            return
          }
        }

        store.commit(type, payload, options);
      }
    };

    // getter和state对象必须延迟获取,因为它们可能会改变,在vm实例更新时
    Object.defineProperties(local, {
      getters: {
        get: noNamespace
          ? function () { return store.getters; }
          : function () { return makeLocalGetters(store, namespace); }
      },
      state: {
        // 通过一层层查找
        get: function () { return getNestedState(store.state, path); }
      }
    });

    return local
  }

注意2:在带命名空间的模块注册全局action

若需要在带命名空间的模块注册全局 action,你可添加 root: true,并将这个 action 的定义放在函数 handler 中。例如:

{
  actions: {
    someOtherAction ({dispatch}) {
      dispatch('someAction')
    }
  },
  modules: {
    foo: {
      namespaced: true,

      actions: {
        someAction: {
          root: true,
          handler (namespacedContext, payload) { ... } // -> 'someAction'
        }
      }
    }
  }
}
  • 核心源码
// 遍历actions,执行回调
module.forEachAction(function (action, key) {
  var type = action.root ? key : namespace + key;
  /**
    两种形式
    actions: {
      increment(context) { },
      decrement: {
        handler: function() { },
        // 扩展 表示当前action注册到根对象上
        root: true 
      }
    }
  */
  var handler = action.handler || action;
  registerAction(store, type, handler, local);
});

注意3:带命名空间的绑定函数

当使用 mapState, mapGetters, mapActionsmapMutations 这些函数来绑定带命名空间的模块时,写起来可能比较繁琐:

computed: {
  ...mapState({
    a: state => state.some.nested.module.a,
    b: state => state.some.nested.module.b
  })
},
methods: {
  ...mapActions([
    'some/nested/module/foo', // -> this['some/nested/module/foo']()
    'some/nested/module/bar' // -> this['some/nested/module/bar']()
  ])
}

对于这种情况,你可以将模块的空间名称字符串作为第一个参数传递给上述函数,这样所有绑定都会自动将该模块作为上下文。于是上面的例子可以简化为:

computed: {
  ...mapState('some/nested/module', {
    a: state => state.a,
    b: state => state.b
  })
},
methods: {
  ...mapActions('some/nested/module', [
    'foo', // -> this.foo()
    'bar' // -> this.bar()
  ])
}
  • 核心源码
/**
 * mapState辅助函数,以减少Vue代码量
 * normalizeNamespace执行会返回一个函数,mapState在执行时需要传递两个参数 
 * 第一个参数可以是字符串(表示命名空间)|| 对象或数组(表示需要映射出来的states)
 * 第二个参数字符串数组或对象。对象属性值可以为字符串或函数,为字符串时直接,直接使用,为函数时,该函数第一个形参为state,第二个形参为getters
 * @param {String} [namespace] - 模块的命名空间 
 * @param { Array<string> | Object<string | function> } states 一个对象或数组
 * @return {Object} 返回一个对象,是通过states解析出来的key|value形式
 * @example
 * 
 * computed: {
 *   ...mapState('some/nested/module', {
 *     a: state => state.a,
 *     b: state => state.b
 *   })
 * }
 * 
 * 即res = { a: fn, b: fn }
 * 因为挂载到了computed上,所以调用时不需要fn加()进行调用
 * 
 */
var mapState = normalizeNamespace(function (namespace, states) {
  var res = {};
  // 期待一个对象或数组
  if ( !isValidMap(states)) {
    console.error('[vuex] mapState: mapper parameter must be either an Array or an Object');
  }
  // normalizeMap格式化states后,变成数组对象key|value形式
  normalizeMap(states).forEach(function (ref) {
    var key = ref.key;
    var val = ref.val;

    res[key] = function mappedState () {
      var state = this.$store.state;
      var getters = this.$store.getters;
      if (namespace) {
        // 根据命名空间获取module
        var module = getModuleByNamespace(this.$store, 'mapState', namespace);
        if (!module) {
          return
        }
        // var local = module.context 本地(局部)的dispatch、commit、getters、state
        state = module.context.state;
        getters = module.context.getters;
      }
      return typeof val === 'function'
        ? val.call(this, state, getters)
      : state[val]
    };
    // mark vuex getter for devtools
    res[key].vuex = true;
  });
  //
  return res
});

/**
 * mapMutations辅助函数,以减少Vue代码量
 * normalizeNamespace执行会返回一个函数,mapMutations在执行时需要传递两个参数 
 * 第一个参数可以是字符串(表示命名空间)|| 对象或数组(表示需要映射出来的states)
 * 第二个参数字符串数组或对象。对象属性值可以为字符串或函数,为字符串时直接,直接使用,为函数时,该函数第一个形参为commit,需要手动触发(传入commit类型)
 * @param {String} [namespace] - namespace 为空时,表示commit根模块的
 * @param { Array<string> | Object<string | function } mutations # 一个对象或数组
 * @return {Object}
 * eg:
 *  methods: {
 *    ...mapActions('some/nested/module', [
 *      'foo', // -> this.foo()
 *      'bar' // -> this.bar()
 *    ]),
 *    
 *    ...mapActions('some/nested/module', [
 *      'fooA': (commit, a) => { // something }, // -> this.fooA('a')
 *      'barB': (commit, a) => { // something } // -> this.barB('a')
 *    ])
 * 
 *  }
 */
var mapMutations = normalizeNamespace(function (namespace, mutations) {
  var res = {};
  if ( !isValidMap(mutations)) {
    console.error('[vuex] mapMutations: mapper parameter must be either an Array or an Object');
  }
  normalizeMap(mutations).forEach(function (ref) {
    var key = ref.key;
    var val = ref.val;

    res[key] = function mappedMutation () {
      var args = [], len = arguments.length;
      while ( len-- ) args[ len ] = arguments[ len ];

      // Get the commit method from store
      var commit = this.$store.commit;
      if (namespace) {
        var module = getModuleByNamespace(this.$store, 'mapMutations', namespace);
        if (!module) {
          return
        }
        commit = module.context.commit;
      }
      return typeof val === 'function'
        ? val.apply(this, [commit].concat(args)) // val为函数时,{ key: (commit) => { } } val执行时第一个参数为commit,后续commit需要自己手动操作
      : commit.apply(this.$store, [val].concat(args)) // 如果val是字符串,则val即为commit的类型,args的payload载荷
    };
  });
  return res
});

/**
 * 它将规范化命名空间,返回一个函数
 * @param {Function} fn
 * @return {Function}
 */
function normalizeNamespace (fn) {
  return function (namespace, map) {
    if (typeof namespace !== 'string') { // 如果函数执行时第一个参数给的不是字符串,那么就把第一个参数当成map对象,命名空间则为""
      map = namespace;
      namespace = '';
    } else if (namespace.charAt(namespace.length - 1) !== '/') { // 如果命名空间字符串最后一位没有"/"的话,为其加上
      namespace += '/';
    }
    return fn(namespace, map)
  }
}

注意4:而且,你可以通过使用 createNamespacedHelpers 创建基于某个命名空间辅助函数。它返回一个对象,对象里有新的绑定在给定命名空间值上的组件绑定辅助函数:

import { createNamespacedHelpers } from 'vuex'

const { mapState, mapActions } = createNamespacedHelpers('some/nested/module')

export default {
  computed: {
    // 在 `some/nested/module` 中查找
    ...mapState({
      a: state => state.a,
      b: state => state.b
    })
  },
  methods: {
    // 在 `some/nested/module` 中查找
    ...mapActions([
      'foo',
      'bar'
    ])
  }
}
  • 核心源码
/**
 * createNamespacedHelpers辅助函数,创建基于某个命名空间辅助函数
 * @param {String} namespace
 * @return {Object} 返回一个对象,包含mapState、mapGetters、mapMutations、mapActions辅助函数
 */
var createNamespacedHelpers = function (namespace) { return ({
  mapState: mapState.bind(null, namespace),
  mapGetters: mapGetters.bind(null, namespace),
  mapMutations: mapMutations.bind(null, namespace),
  mapActions: mapActions.bind(null, namespace)
}); };

注意5:模块动态注册

在 store 创建之后,你可以使用 store.registerModule 方法注册模块:

import Vuex from 'vuex'

const store = new Vuex.Store({ /* 选项 */ })

// 注册模块 `myModule`
store.registerModule('myModule', {
  // ...
})
// 注册嵌套模块 `nested/myModule`
store.registerModule(['nested', 'myModule'], {
  // ...
})
  • 核心源码
/** 4、注册module */
ModuleCollection.prototype.register = function register (path, rawModule, runtime) {
  var this$1 = this;
	// ...
  var newModule = new Module(rawModule, runtime);
  if (path.length === 0) {
    this.root = newModule; // 根module
  } else {
    // path.slice(0, -1)获取除了数组最后一位的所有项
    // ModuleCollection.prototype.get返回指定命名空间下的module,path为空时,返回this.root
    var parent = this.get(path.slice(0, -1));
    // 将path数组中的最后一位(也就是自己),绑定到parent的_children对象上
    parent.addChild(path[path.length - 1], newModule);
  }

  // 程序员传递的options有modules时
  if (rawModule.modules) {
    forEachValue(rawModule.modules, function (rawChildModule, key) {
      // 注册子模块,path数组添加modules的属性名
      this$1.register(path.concat(key), rawChildModule, runtime);
    });
  }
};

注意6:保留state

在注册一个新 module 时,你很有可能想保留过去的 state,例如从一个服务端渲染的应用保留 state。你可以通过 preserveState 选项将其归档:store.registerModule('a', module, { preserveState: true })

当你设置 preserveState: true 时,该模块会被注册,action、mutation 和 getter 会被添加到 store 中,但是 state 不会。这里假设 store 的 state 已经包含了这个 module 的 state 并且你不希望将其覆写。

  • 核心源码
/** 7、程序员调用的registerModule | 注册模块 */
Store.prototype.registerModule = function registerModule (path, rawModule, options) {
  if ( options === void 0 ) options = {};

  if (typeof path === 'string') { path = [path]; }

  {
    assert(Array.isArray(path), "module path must be a string or an Array.");
    // path为空数组时,发出警告。不能通过此方法注册根模块
    assert(path.length > 0, 'cannot register the root module by using registerModule.');
  }
  // 调用ModuleCollection.Prototype.register进行注册(先注册)
  this._modules.register(path, rawModule);

  // 安装模块 | preserveState保存状态为true时,不把当前模块的state添加到父级模块的state下(后续也就无法获取)
  installModule(this, this.state, path, this._modules.get(path), options.preserveState);
  // 重置store实例,从新设置了$$state
  resetStoreVM(this, this.state);
};

/**
 * 安装modules
 * @param {Object} store 
 * @param {Object} rootState 根state
 * @param {Array} path 模块路径 ['feature', 'character']
 * @param {Object} module 模块
 * @param {Boolean} hot 
 */
function installModule(store, rootState, path, module, hot) {
  // 是否是根模块
  var isRoot = !path.length;
  // 获取模块的空间名称 eg: 'feature/character/'
  var namespace = store._modules.getNamespace(path);
	// ...
  // (注册子模块时 && 执行Store.prototype.registerModule注册模块时,如果传入的options.preserveState为undefined时)通过判断
  if (!isRoot && !hot) {
    /**
      父级state,也就是说,根state保存的是一个对象,里面有子对象,保存的是子模块的state
      eg:
      feature.state = parentState = {
        age: '18',
        character: {
          height: '170cm'
        }
      }
   */
    var parentState = getNestedState(rootState, path.slice(0, -1));
    // 子模块名称
    var moduleName = path[path.length - 1];
    store._withCommit(function () {
      {
        if (moduleName in parentState) {
          console.warn(
            ("[vuex] state field \"" + moduleName + "\" was overridden by a module with the same name at \"" + (path.join('.')) + "\"")
          );
        }
      }
      // 给父级state添加属性,属性名为子模块module名,值为子模块的state 
      // 通过赋值以后,getNestedState就能从根state向子模块的state中取值 ---------- important-api-state 的使用
      Vue.set(parentState, moduleName, module.state);
    });
  }
	// ...
}

模块重用

有时我们可能需要创建一个模块的多个实例,例如:

  • 创建多个store,他们公用同一个模块
  • 在一个store中多次注册同一个模块

如果我们使用一个纯对象来声明模块的状态,那么这个状态对象会通过引用被共享,导致状态对象被修改时 store 或模块间数据互相污染的问题。

实际上这和 Vue 组件内的 data 是同样的问题。因此解决办法也是相同的——使用一个函数来声明模块状态(仅 2.3.0+ 支持):

const MyReusableModule = {
  state: () => ({
    foo: 'bar'
  }),
  // mutation, action 和 getter 等等...
}
  • 核心源码
// 存储模块
var Module = function Module (rawModule, runtime) {
  // ...
  // 确保options上的state为一个对象
  this.state = (typeof rawState === 'function' ? rawState() : rawState) || {};
};

ModuleCollection.prototype.register = function register (path, rawModule, runtime) {
  var this$1 = this;
	// ...
  var newModule = new Module(rawModule, runtime);
  // ...
};

plugins

一个数组,包含应用在 store 上的插件方法。这些插件直接接收 store 作为唯一参数,可以监听 mutation(用于外部地数据持久化、记录或调试)或者提交 mutation (用于内部数据,例如 websocket 或 某些观察者)

  • 类型:Array<Function>

Vuex 的 store 接受 plugins 选项,这个选项暴露出每次 mutation 的钩子。Vuex 插件就是一个函数,它接收 store 作为唯一参数:

const myPlugin = store => {
  // 当 store 初始化后调用
  store.subscribe((mutation, state) => {
    // 每次 mutation 之后调用
    // mutation 的格式为 { type, payload }
  })
}
  • 使用方式
const store = new Vuex.Store({
  // ...
  plugins: [myPlugin]
})

有时候插件需要获得状态的“快照”,比较改变的前后状态。想要实现这项功能,你需要对状态对象进行深拷贝:

const myPluginWithSnapshot = store => {
  let prevState = _.cloneDeep(store.state)
  store.subscribe((mutation, state) => {
    let nextState = _.cloneDeep(state)

    // 比较 prevState 和 nextState...

    // 保存状态,用于下一次 mutation
    prevState = nextState
  })
}

内置 Logger 插件

Vuex 自带一个日志插件用于一般的调试:

import createLogger from 'vuex/dist/logger'

const store = new Vuex.Store({
  plugins: [createLogger()]
})

createLogger 函数有几个配置项:

const logger = createLogger({
  collapsed: false, // 自动展开记录的 mutation
  filter (mutation, stateBefore, stateAfter) {
    // 若 mutation 需要被记录,就让它返回 true 即可
    // 顺便,`mutation` 是个 { type, payload } 对象
    return mutation.type !== "aBlocklistedMutation"
  },
  actionFilter (action, state) {
    // 和 `filter` 一样,但是是针对 action 的
    // `action` 的格式是 `{ type, payload }`
    return action.type !== "aBlocklistedAction"
  },
  transformer (state) {
    // 在开始记录之前转换状态
    // 例如,只返回指定的子树
    return state.subTree
  },
  mutationTransformer (mutation) {
    // mutation 按照 { type, payload } 格式记录
    // 我们可以按任意方式格式化
    return mutation.type
  },
  actionTransformer (action) {
    // 和 `mutationTransformer` 一样,但是是针对 action 的
    return action.type
  },
  logActions: true, // 记录 action 日志
  logMutations: true, // 记录 mutation 日志
  logger: console, // 自定义 console 实现,默认为 `console`
})
  • 核心源码
var Store = function (options) {
  // ...
  // 插件数组
  var plugins = options.plugins; if ( plugins === void 0 ) plugins = [];
  // ...
  // 执行plugins,传入store
  plugins.forEach(function (plugin) { return plugin(this$1); });
  // ...
}

strict

在严格模式下,无论何时发生了状态变更且不是由 mutation 函数引起的,将会抛出错误。

  • 类型:boolean
  • 默认值:false
const store = new Vuex.Store({
  // ...
  strict: true
})
  • 核心源码
/**
 * 开启严格模式,监听$$state的改变
 */
function enableStrictMode (store) {
  // 监听$$state改变时触发回调 利用$$state来存储state
  // Vue.prototype.$watch(expOrFn, cb, options)
  // deep表示深度监听,sync表示立即执行watch中的回调方法,不需要等到视图渲染完毕
  store._vm.$watch(function () { return this._data.$$state }, function () {
    {
      // store._committing默认为false,通过mutation函数引起的将会触发_withCommit将_committing置为true
      // 也就是说,在开启严格模式时,在外部更改$$state会发出警告
      assert(store._committing, "do not mutate vuex store state outside mutation handlers.");
    }
  }, { deep: true, sync: true });
}

devtools

为某个特定的 Vuex 实例打开或关闭 devtools。对于传入 false 的实例来说 Vuex store 不会订阅到 devtools 插件。可用于一个页面中有多个 store 的情况。

  • 类型:boolean
const store = new Vuex.Store({
  // ...
  devtools: false
})
  • 核心源码
var Store = function (options) {
  // ...
  // 开启调试工具
  var useDevtools = options.devtools !== undefined ? options.devtools : Vue.config.devtools;
  if (useDevtools) {
    devtoolPlugin(this);
  }
  // ...
}

replaceState

替换 store 的根状态,仅用状态合并或时光旅行调试。

  • replaceState(state: Object)
  • 核心源码
/** 6、程序员调用的replaceState | 重置state */
Store.prototype.replaceState = function replaceState (state) {
  var this$1 = this;

  this._withCommit(function () {
    this$1._vm._data.$$state = state;
  });
};

watch

响应式地侦听 fn 的返回值,当值改变时调用回调函数。fn 接收 store 的 state 作为第一个参数,其 getter 作为第二个参数。最后接收一个可选的对象参数表示 Vue 的 vm.$watch 方法的参数。

要停止侦听,调用此方法返回的函数即可停止侦听。

  • watch(fn: Function, callback: Function, options?: Object): Function
  • 核心源码
/** 5、程序员调用的watch | 监听数据 */
Store.prototype.watch = function watch (getter, cb, options) {
  var this$1 = this;

  {
    // 期待的第一个参数类型为Function
    assert(typeof getter === 'function', "store.watch only accepts a function.");
  }
  // 实际上还是得调用Vue的$watch
  // 第一个参数执行时会接收到两个参数,第一个为state,第二个为getters
  // 第一个参数必须要使用到响应式数据时,才会触发cb回调,cb接收两个参数,第一个为newVal,第二个为oldVal
  // eg: getter = function (state, getters) { return state.name + state.age },每当返回值不同时,都会触发cb
  return this._watcherVM.$watch(function () { return getter(this$1.state, this$1.getters); }, cb, options)
};

// Vue的$watch方法
Vue.prototype.$watch = function (expOrFn, cb, options) {
  var vm = this;
  // 判断是否是对象 如果是对象则递归 深层 监听 直到它不是一个对象的时候才会跳出递归
  if (isPlainObject(cb)) {
    return createWatcher(vm, expOrFn, cb, options)
  }
  options = options || {};
  options.user = true;
  var watcher = new Watcher(vm, expOrFn, cb, options);
  if (options.immediate) {
    try {
      cb.call(vm, watcher.value);
    } catch (error) {
      handleError(error, vm, ("callback for immediate watcher \"" + (watcher.expression) + "\""));
    }
  }
  // $watch返回一个函数,该函数执行时,即可停止监听!
  return function unwatchFn() {
    watcher.teardown();
  }
}

subscribe

订阅 store 的 mutation。handler 会在每个 mutation 完成后调用(详见:commit),接收 mutation 和经过 mutation 后的状态作为参数:

  • subscribe(handler: Function, options?: Object): Function

  • 使用方式

store.subscribe((mutation, state) => {
  console.log(mutation.type)
  console.log(mutation.payload)
})
  • 核心源码
/** 3、程序员调用的subscribe */
/**
 * 统一订阅commit,每次调用commit时,都会把store._subscribers数组中的所有函数取出并执行
 * 相当于commit的回调
 * @param {Function} fn 回调
 * @param {Object} options 只支持 { prepend: true // 表示往数组前面增加,触发顺序靠前 }
 */
Store.prototype.subscribe = function subscribe (fn, options) {
  return genericSubscribe(fn, this._subscribers, options)
};

/**
 * 统一订阅 | 将fn添加进subs数组中,options对象的可选属性有prepend(即:往订阅的数组前面添加)
 * @param {Function | Object} fn 回调函数 | 对象
 * @param {Array} subs 订阅的数组
 * @param {Object} options options对象
 * @return 返回一个函数,执行这个回调会把传入的fn从subs数组中移除
 */
function genericSubscribe (fn, subs, options) {
  if (subs.indexOf(fn) < 0) {
    options && options.prepend
      ? subs.unshift(fn)
    : subs.push(fn);
  }
  return function () {
    var i = subs.indexOf(fn);
    if (i > -1) {
      subs.splice(i, 1);
    }
  }
}

默认情况下,新的处理函数会被添加到其链的尾端,因此它会在其它之前已经被添加了的处理函数之后执行。这一行为可以通过向 options 添加 prepend: true 来覆写,即把处理函数添加到其链的最开始。

store.subscribe(handler, { prepend: true })

要停止订阅,调用此方法返回的函数即可停止订阅。

subscribeAction

订阅 store 的 action。handler 会在每个 action 分发的时候调用(详见:action)并接收 action 描述和当前的 store 的 state 这两个参数:

  • subscribeAction(handler: Function, options?: Object): Function
store.subscribeAction((action, state) => {
  console.log(action.type)
  console.log(action.payload)
})
  • 核心源码
/** 4、程序员调用的subscribeAction */
/**
 * 统一订阅dispatch,每次调用dispatch时,都会把store._actionSubscribers数组中的所有函数取出并执行
 * 相当于dispatch的回调
 * @param {Function | Object} fn 回调 
 * eg: { before: fn, after: fn, error: fn } 
 * 其中before会在actions执行前触发,after会在actions执行后返回的promise对象中触发,error为错误时触发
 * 
 * @param {Object} options 只支持 { prepend: true // 表示往数组前面增加,触发顺序靠前 }
 */
Store.prototype.subscribeAction = function subscribeAction (fn, options) {
  var subs = typeof fn === 'function' ? { before: fn } : fn;
  return genericSubscribe(subs, this._actionSubscribers, options)
};

从 3.1.0 起,subscribeAction 也可以指定订阅处理函数的被调用时机应该在一个 action 分发之前还是之后 (默认行为是之前):

store.subscribeAction({
  before: (action, state) => {
    console.log(`before action ${action.type}`)
  },
  after: (action, state) => {
    console.log(`after action ${action.type}`)
  }
})

自 3.4.0 起,subscribeAction 也可以指定一个 error 处理函数以捕获分发 action 的时候被抛出的错误。该函数会从第三个参数接收到一个 error 对象。

store.subscribeAction({
  error: (action, state, error) => {
    console.log(`error action ${action.type}`)
    console.error(error)
  }
})

registerModule

动态一个动态模块。

  • registerModule(path: string | Array<string>, module: Module, options?: Object)
  • options 可以包含 preserveState: true 以允许保留之前的 state。用于服务端渲染。
  • 核心源码
/** 7、程序员调用的registerModule | 注册模块 */
Store.prototype.registerModule = function registerModule (path, rawModule, options) {
  if ( options === void 0 ) options = {};

  if (typeof path === 'string') { path = [path]; }

  {
    assert(Array.isArray(path), "module path must be a string or an Array.");
    // path为空数组时,发出警告。不能通过此方法注册根模块
    assert(path.length > 0, 'cannot register the root module by using registerModule.');
  }
  // 调用ModuleCollection.Prototype.register进行注册(先注册)
  this._modules.register(path, rawModule);

  // 安装模块 | preserveState保存状态为true时,不把当前模块的state添加到父级模块的state下(后续也就无法获取)
  installModule(this, this.state, path, this._modules.get(path), options.preserveState);
  // 重置store实例,重新设置了$$state
  resetStoreVM(this, this.state);
};

unregisterModule

卸载一个动态模块。

  • unregisterModule(path: string | Array<string>)
  • 核心源码
/** 8、程序员调用的unregisterModule | 卸载模块 */
Store.prototype.unregisterModule = function unregisterModule (path) {
  var this$1 = this;

  if (typeof path === 'string') { path = [path]; }

  {
    assert(Array.isArray(path), "module path must be a string or an Array.");
  }

  // 调用ModuleCollection.Prototype.unregister进行卸载
  this._modules.unregister(path);
  this._withCommit(function () {
    var parentState = getNestedState(this$1.state, path.slice(0, -1));
    Vue.delete(parentState, path[path.length - 1]);
  });
  resetStore(this);
};

hasModule

检查该模块的名字是否已经被注册。

  • hasModule(path: string | Array<string>): boolean
  • 核心源码
/** 8、程序员调用的hasModule | 判断是否具有模块 */
Store.prototype.hasModule = function hasModule (path) {
  if (typeof path === 'string') { path = [path]; }

  {
    assert(Array.isArray(path), "module path must be a string or an Array.");
  }

  // 调用ModuleCollection.Prototype.isRegistered进行判断
  return this._modules.isRegistered(path)
};

hotUpdate

热替换新的 action 和 mutation。

  • hotUpdate(newOptions: Object)
  • 核心源码
/** 9、程序员调用的hotUpdate | 热更新 */
Store.prototype.hotUpdate = function hotUpdate (newOptions) {
  this._modules.update(newOptions);
  resetStore(this, true);
};

/**
 * 重置Store(也叫初始化)
 */
function resetStore (store, hot) {
  store._actions = Object.create(null);
  store._mutations = Object.create(null);
  store._wrappedGetters = Object.create(null);
  store._modulesNamespaceMap = Object.create(null);
  var state = store.state;
  // 重新初始化模块
  installModule(store, state, [], store._modules.root, true);
  // 重置StoreVM
  resetStoreVM(store, state, hot);
}

/**
 * 重置store实例
 * @param {Object} store Store实例
 * @param {Object} state store.state
 * @param {*} hot 
 */
function resetStoreVM (store, state, hot) {
  var oldVm = store._vm;
	// ...
  // 初始化时不执行
  if (oldVm) {
    if (hot) {
      // 在热更新模式下,更改旧Vm的$$state,以达到发布通知,更改视图
      store._withCommit(function () {
        oldVm._data.$$state = null;
      });
    }
    // 销毁旧VM
    Vue.nextTick(function () { return oldVm.$destroy(); });
  }
}

组件绑定的辅助函数

辅助函数将会用到的公共方法

/**
 * 它将规范化命名空间,返回一个函数
 * @param {Function} fn
 * @return {Function}
 */
function normalizeNamespace (fn) {
  return function (namespace, map) {
    if (typeof namespace !== 'string') { // 如果函数执行时第一个参数给的不是字符串,那么就把第一个参数当成map对象,命名空间则为""
      map = namespace;
      namespace = '';
    } else if (namespace.charAt(namespace.length - 1) !== '/') { // 如果命名空间字符串最后一位没有"/"的话,为其加上
      namespace += '/';
    }
    return fn(namespace, map)
  }
}

/**
 * 格式化map,变成数组对象key|value形式
 * 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(function (key) { return ({ key: key, val: key }); })
  : Object.keys(map).map(function (key) { return ({ key: key, val: map[key] }); })
}

/**
 * 按命名空间从存储区搜索特殊模块。如果模块不存在,则打印错误消息。
 * @param {Object} store
 * @param {String} helper
 * @param {String} namespace
 * @return {Object}
 */
function getModuleByNamespace (store, helper, namespace) {
  var module = store._modulesNamespaceMap[namespace];
  if ( !module) {
    console.error(("[vuex] module namespace not found in " + helper + "(): " + namespace));
  }
  return module
}

mapState

  • mapState(namespace?: string, map: Array<string> | Object<string | function>): Object

为组件创建计算属性以返回 Vuex store 中的状态。

第一个参数是可选的,可以是一个命名空间字符串。

对象形式的第二个参数的成员可以是一个函数。

  • 使用方式
// 在单独构建的版本中辅助函数为 Vuex.mapState
import { mapState } from 'vuex'

export default {
  // ...
  computed: mapState({
    // 箭头函数可使代码更简练
    count: state => state.count,

    // 传字符串参数 'count' 等同于 `state => state.count`
    countAlias: 'count',

    // 为了能够使用 `this` 获取局部状态,必须使用常规函数
    countPlusLocalState (state) {
      return state.count + this.localCount
    }
  })
}

当映射的计算属性的名称与 state 的子节点名称相同时,我们也可以给 mapState 传一个字符串数组。

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

mapState 函数返回的是一个对象。我们如何将它与局部计算属性混合使用呢?通常,我们需要使用一个工具函数将多个对象合并为一个,以使我们可以将最终对象传给 computed 属性。但是自从有了对象展开运算符,我们可以极大地简化写法:

computed: {
  localComputed () { /* ... */ },
  // 使用对象展开运算符将此对象混入到外部对象中
  ...mapState({
    // ...
  })
}
  • 核心源码
/**
 * mapState辅助函数,以减少Vue代码量
 * normalizeNamespace执行会返回一个函数,mapState在执行时需要传递两个参数 
 * 第一个参数可以是字符串(表示命名空间)|| 对象或数组(表示需要映射出来的states)
 * 第二个参数字符串数组或对象。对象属性值可以为字符串或函数,为字符串时直接,直接使用,为函数时,该函数第一个形参为state,第二个形参为getters
 * @param {String} [namespace] - 模块的命名空间 
 * @param { Array<string> | Object<string | function> } states 一个对象或数组
 * @return {Object} 返回一个对象,是通过states解析出来的key|value形式
 * @example
 * 
 * computed: {
 *   ...mapState('some/nested/module', {
 *     a: state => state.a,
 *     b: state => state.b
 *   })
 * }
 * 
 * 即res = { a: fn, b: fn }
 * 因为挂载到了computed上,所以调用时不需要fn加()进行调用
 * 
 */
var mapState = normalizeNamespace(function (namespace, states) {
  var res = {};
  // 期待一个对象或数组
  if ( !isValidMap(states)) {
    console.error('[vuex] mapState: mapper parameter must be either an Array or an Object');
  }
  // normalizeMap格式化states后,变成数组对象key|value形式
  normalizeMap(states).forEach(function (ref) {
    var key = ref.key;
    var val = ref.val;

    res[key] = function mappedState () {
      var state = this.$store.state;
      var getters = this.$store.getters;
      if (namespace) {
        // 根据命名空间获取module
        var module = getModuleByNamespace(this.$store, 'mapState', namespace);
        if (!module) {
          return
        }
        // var local = module.context 本地(局部)的dispatch、commit、getters、state
        state = module.context.state;
        getters = module.context.getters;
      }
      return typeof val === 'function'
        ? val.call(this, state, getters)
      : state[val]
    };
    // mark vuex getter for devtools
    res[key].vuex = true;
  });
  //
  return res
});

mapGetters

  • mapGetters(namespace?: string, map: Array<string> | Object<string>): Object

为组件创建计算属性以返回 getter 的返回值。

第一个参数是可选的,可以是一个命名空间字符串。

  • 使用方式
import { mapGetters } from 'vuex'

export default {
  // ...
  computed: {
  // 使用对象展开运算符将 getter 混入 computed 对象中
    ...mapGetters([
      'doneTodosCount',
      'anotherGetter',
      // ...
    ])
  }
}

如果你想将一个 getter 属性另取一个名字,使用对象形式:

...mapGetters({
  // 把 `this.doneCount` 映射为 `this.$store.getters.doneTodosCount`
  doneCount: 'doneTodosCount'
})
  • 核心源码
/**
 * mapGetters辅助函数,以减少Vue代码量
 * normalizeNamespace执行会返回一个函数,mapGetters在执行时需要传递两个参数 
 * 第一个参数可以是字符串(表示命名空间)|| 对象或数组(表示需要映射出来的states)
 * 第二个参数字符串数组或对象。对象属性值只能是字符串
 * @param { String } [namespace] - 模块的命名空间 
 * @param { Array<string> | Object<string> } getters # 一个对象或数组
 * @return {Object}
 */
var mapGetters = normalizeNamespace(function (namespace, getters) {
  var res = {};
  if ( !isValidMap(getters)) {
    console.error('[vuex] mapGetters: mapper parameter must be either an Array or an Object');
  }
  normalizeMap(getters).forEach(function (ref) {
    var key = ref.key;
    var val = ref.val;

    // getters的取值需要完整的命名空间
    val = namespace + val;
    res[key] = function mappedGetter () {
      if (namespace && !getModuleByNamespace(this.$store, 'mapGetters', namespace)) {
        return
      }
      if ( !(val in this.$store.getters)) {
        console.error(("[vuex] unknown getter: " + val));
        return
      }
      return this.$store.getters[val]
    };
    // mark vuex getter for devtools
    res[key].vuex = true;
  });
  return res
});

mapActions

  • mapActions(namespace?: string, map: Array<string> | Object<string | function>): Object

创建组件方法分发 action。

第一个参数是可选的,可以是一个命名空间字符串。

对象形式的第二个参数的成员可以是一个函数。

  • 使用方式

在组件中分发 Action

import { mapActions } from 'vuex'

export default {
  // ...
  methods: {
    ...mapActions([
      'increment', // 将 `this.increment()` 映射为 `this.$store.dispatch('increment')`

      // `mapActions` 也支持载荷:
      'incrementBy' // 将 `this.incrementBy(amount)` 映射为 `this.$store.dispatch('incrementBy', amount)`
    ]),
    ...mapActions({
      add: 'increment' // 将 `this.add()` 映射为 `this.$store.dispatch('increment')`
    })
  }
}
  • 核心源码
/**
 * mapActions辅助函数,以减少Vue代码量
 * normalizeNamespace执行会返回一个函数,mapActions在执行时需要传递两个参数 
 * 第一个参数可以是字符串(表示命名空间)|| 对象或数组(表示需要映射出来的states)
 * 第二个参数字符串数组或对象。对象属性值可以为字符串或函数,为字符串时直接,直接使用,为函数时,该函数第一个形参为dispatch,需要手动触发(传入dispatch类型)
 * @param {String} [namespace] - 模块的命名空间 
 * @param { Array<string> | Object<string | function } actions # 一个对象或数组
 * @return {Object}
 */
var mapActions = normalizeNamespace(function (namespace, actions) {
  var res = {};
  if ( !isValidMap(actions)) {
    console.error('[vuex] mapActions: mapper parameter must be either an Array or an Object');
  }
  normalizeMap(actions).forEach(function (ref) {
    var key = ref.key;
    var val = ref.val;

    res[key] = function mappedAction () {
      var args = [], len = arguments.length;
      while ( len-- ) args[ len ] = arguments[ len ];

      // get dispatch function from store
      var dispatch = this.$store.dispatch;
      if (namespace) {
        var module = getModuleByNamespace(this.$store, 'mapActions', namespace);
        if (!module) {
          return
        }
        dispatch = module.context.dispatch;
      }
      return typeof val === 'function'
        ? val.apply(this, [dispatch].concat(args))
      : dispatch.apply(this.$store, [val].concat(args))
    };
  });
  return res
});

带命名空间的绑定函数

当使用 mapState, mapGetters, mapActionsmapMutations 这些函数来绑定带命名空间的模块时,写起来可能比较繁琐:

computed: {
  ...mapState({
    a: state => state.some.nested.module.a,
    b: state => state.some.nested.module.b
  })
},
methods: {
  ...mapActions([
    'some/nested/module/foo', // -> this['some/nested/module/foo']()
    'some/nested/module/bar' // -> this['some/nested/module/bar']()
  ])
}

对于这种情况,你可以将模块的空间名称字符串作为第一个参数传递给上述函数,这样所有绑定都会自动将该模块作为上下文。于是上面的例子可以简化为:

computed: {
  ...mapState('some/nested/module', {
    a: state => state.a,
    b: state => state.b
  })
},
methods: {
  ...mapActions('some/nested/module', [
    'foo', // -> this.foo()
    'bar' // -> this.bar()
  ])
}

createNamespacedHelpers

  • createNamespacedHelpers(namespace: string): Object

创建基于命名空间的组件绑定辅助函数。其返回一个包含 mapStatemapGettersmapActionsmapMutations 的对象。它们都已经绑定在了给定的命名空间上。

  • 使用方式
import { createNamespacedHelpers } from 'vuex'

const { mapState, mapActions } = createNamespacedHelpers('some/nested/module')

export default {
  computed: {
    // 在 `some/nested/module` 中查找
    ...mapState({
      a: state => state.a,
      b: state => state.b
    })
  },
  methods: {
    // 在 `some/nested/module` 中查找
    ...mapActions([
      'foo',
      'bar'
    ])
  }
}
  • 核心源码
/**
 * createNamespacedHelpers辅助函数,创建基于某个命名空间辅助函数
 * @param {String} namespace
 * @return {Object} 返回一个对象,包含mapState、mapGetters、mapMutations、mapActions辅助函数
 */
var createNamespacedHelpers = function (namespace) { return ({
  mapState: mapState.bind(null, namespace),
  mapGetters: mapGetters.bind(null, namespace),
  mapMutations: mapMutations.bind(null, namespace),
  mapActions: mapActions.bind(null, namespace)
}); };

附录: github.com/fanqiewa/Vu…