Vuex3源码学习笔记之Vuex的Map语法糖

在 Vuex 安装在 Vue 的过程中,会把用户定义的 store 实例赋值给 Vue 实例对象的 $store 中,所以我们在 Vue 组件中的任意地方使用 this.$store 调用 store 的值。但是这样使用可读性并不是很好,这

目录
  1. mapState
    1. normalizeNamespace规范化参数
    2. normalizeMap规范化map
  2. mapMutations
  3. mapGetters
  4. mapActions
  5. createNamespacedHelpers
  6. 参考资料

在 Vuex 安装在 Vue 的过程中,会把用户定义的store实例赋值给 Vue 实例对象的$store中,所以我们在 Vue 组件中的任意地方使用this.$store调用store的值。但是这样使用可读性并不是很好,这样不好查找在组件的哪些地方,调用了哪些store上的属性和方法。

一般我们都会使用MapXxx语法糖来引入store的属性,这样很好的查找在哪里使用了store的哪些数据,可读性也会更强,也更方便管理。

mapState#

先看下mapState函数的代码,在helpers.js文件中。

export const mapState = normalizeNamespace((namespace, states) => {
  const res = {};
  normalizeMap(states).forEach(({ key, val }) => {
    // ...
  });
  return res;
});

在分析mapState代码时,需要先搞清楚normalizeNamespacenormalizeMap这两个函数的作用,不然会一脸懵逼。

normalizeNamespace规范化参数#

先看normalizeNamespace函数的代码,这个函数的作用是规范用户传入的参数。

function normalizeNamespace(fn) {
  return (namespace, map) => {
    // ! 命名空间不为字符串。
    // ! 比如,传入 root 的值时,没有模块名,是直接传 { a: mutationName } 或者 [ mutationName ]
    if (typeof namespace !== "string") {
      map = namespace; // ! 把命名空间设置为 map
      namespace = ""; // ! 命名空间为空
      // ! 命名空间没有以 / 结尾时,拼接 / => moduleName = moduleName/
      // ! 模块名和 type 之间需要使用 / 隔开
    } else if (namespace.charAt(namespace.length - 1) !== "/") {
      namespace += "/";
    }
    return fn(namespace, map);
  };
}

这个函数返回的也是一个函数,而且这个函数和我们传入的fn参数具有相同的参数namespacemap,这样就可以很好的约束fn的参数。

首先判断第一个参数namespace是不是字符串,如果不是字符串,那么传入的值就是一个对象或者数组。这时参数namespace就不再是命名空间了,而是把它当成第二个参数map,而第一个参数namespace的值就是空字符串。

如果第一个参数namespace的值是字符串类型,那么它就是命名空间的模块名,这时候判断模块名是不是以/符号结尾,如果不是的话,需要在模块名后面添加/符号,这样做主要是为了在后面更好的拼接type值。

最后再次调用fn函数,调用时把处理好的两个参数namespacemap传入进去。使用normalizeNamespace函数可以很好的规范namespacemap的值。

normalizeMap规范化map#

接下来看下normalizeMap函数的代码。

/**
 * 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}
 * ! 规范化 Map 👆
 */
function normalizeMap(map) {
  return Array.isArray(map)
    ? map.map((key) => ({ key, val: key })) // ! 不修改 key 的名字
    : Object.keys(map).map((key) => ({ key, val: map[key] })); // ! 映射,修改 key的名字
}

这个函数的作用是规范传入map参数的,官方注释已经解释的非常清楚了。它会把数组类型或者对象类型的map转统一换成一个{key, value}结构的对象组成的数组。

比如我们使用mapState时,一般是这样使用的。

computed: {
  ...mapState(['value1', 'value2']),
  ...mapState({val3: 'value3', val4: 'val4'}),
  ...mapState('moduleA', ['value1', 'value2']),
  ...mapState('moduleB', {val3: 'value3', val4: 'value4'})
}

规范map后就变成下面这样子。

computed: {
  ...mapState([{ key:'value1',val:'value1'}, { key:'value2',val:'value2' }]),
  ...mapState([{ key: val3: val: 'value3' }, { key: val4: val: 'val4' }]),
  ...mapState('moduleA/', [{ key:'value1',val:'value1'}, { key:'value2',val:'value2' }]),
  ...mapState('moduleB/', [{ key: val3: val: 'value3' }, { key: val4: val: 'val4' }])
}

现在我们可以再看mapState的代码。

export const mapState = normalizeNamespace((namespace, states) => {
  const res = {};
  // ! states: [1, 2, 3] => [{ key: 1, val: 1 }, { key: 2, val: 2 }, { key: 3, val: 3 }]
  // ! states: {a:1, b:2, c:3} => [{ key: a, val: 1 }, { key: b, val: 2 }, { key: c, val: 3 }]
  normalizeMap(states).forEach(({ key, val }) => {
    res[key] = function mappedState() {
      // ! 获取 root 上的值
      let state = this.$store.state;
      let getters = this.$store.getters;
      // ! 如果设置了命名空间,即 mapXXX(namespace, ['name1', 'name2'])
      // ! 获取命名空间模块的值,即 store.state.namespace.name1
      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) // ! val(state, getters)
        : state[val]; // ! 返回 state 中对应的值即可
    };
    // mark vuex getter for devtools
    res[key].vuex = true;
  });
  return res;
});

首先声明了变量res,它的初始值是空对象,最后会返回这个值。然后遍历states,就是上面规范后的map的值。然后在res对象中生成一对对{key: mappedState}键值对结构的数据。

下面主要看下mappedState函数的代码。

首先声明变量state存储 root 上的state,声明变量getters存储 root 上的getters,如果有命名空间的模块后,会获通过命名空间的模块名获取到这个模块,然后变量stategetters会变成模块上下文中的stategetters

再判断val的值是否是函数,有时候会通过调用函数生成数据。如果是函数的话,会使用call方法来调用,this指向组件自身,然后把stategetters作为参数传入,如果不是函数,一般就是字符串,会返回state中对应val的值。

我们一般使用时传入字符串类型的值多一点,看下面的代码。

mapState("moduleA", ["value"]);

这时会返回state[val],然后重新查看在模块上下文中是怎么修改state的值。

Object.defineProperties(local, {
  state: {
    get: () => getNestedState(store.state, path), // ! 通过路径获取嵌套的 state
  },
});

也就是说会使用getNestedState方法,通过路径path获取对应的state的值。而这里有命名空间模块,那么path的值就是['moduleA','value'],这样就可以获取到模块中value的值。

mapMutations#

下面看下mapMutations函数的代码。

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; // ! 根的 commit
      if (namespace) {
        // ! 获取模块
        const module = getModuleByNamespace(
          this.$store,
          "mapMutations",
          namespace
        );
        if (!module) {
          return;
        }
        commit = module.context.commit; // ! 模块的 commit
      }
      return typeof val === "function"
        ? // ! 调用这个函数 val(commit, args),函数传入 commit,在函数体中可以使用 commit 来提交其它 mutation
          val.apply(this, [commit].concat(args))
        : commit.apply(this.$store, [val].concat(args)); // ! string 形式 --> this.$store.commit(val, ...args)
    };
  });
  return res;
});

逻辑和mapState大同小异,有命名空间模块的时候,获取到的是模块上下文中的commit方法。

然后判断val是否为函数,如何是的话,会调用这个函数,并且传入commit和参数args。这里为什么要传入commit,因为在函数体中可以使用commit来提交其它的mutation函数。如果val不是函数,那应该是字符串,这时会调用commit函数,this指向的this.$store,然后传入val和参数args,这就相当于是下面这样调用的。

this.$store.commit(val, ...args);

我们发现mapMutations并不是简单的映射mutation的函数名,而是映射一个包装函数,这个函数里面使用commit来提交mutation函数,用户在组件中使用这个函数就相当于提交mutation函数,而不需要在组件中再次显示使用commit来提交,这样设计是非常人性化的。

mapGetters#

下面看下mapGetters函数的代码。

export const mapGetters = normalizeNamespace((namespace, getters) => {
  const res = {};
  normalizeMap(getters).forEach(({ key, val }) => {
    // The namespace has been mutated by normalizeNamespace
    val = namespace + val; // ! moduleName/getterName
    res[key] = function mappedGetter() {
      if (
        namespace &&
        !getModuleByNamespace(this.$store, "mapGetters", namespace)
      ) {
        return;
      }
      if (
        process.env.NODE_ENV !== "production" &&
        !(val in this.$store.getters)
      ) {
        console.error(`[vuex] unknown getter: ${val}`);
        return;
      }
      return this.$store.getters[val]; // ! 根据拼接后的 val 从实例属性 getters 获取对应的值
    };
    // mark vuex getter for devtools
    res[key].vuex = true;
  });
  return res;
});

mapGetters的实现和mapState有一些不同,主要是Storegetters属性和state属性的结构不一样。

// state
state: {
  count: 0 // root state
  moduleA: {
    count: 1
  },
  moduleB: {
    count: 2
  }
}
 
// getters
getters: {
  getterFn(){}, // root getters
  'modulesA/getterFn'() {},
  'modulesA/getterFn2'() {},
  'modulesB/getterFn'() {}
}

getters采用字符串拼接的方式,使得val的值为moduleName/getterName,然后在store.getters中直接通过 key 获取值。

mapActions#

export const mapActions = normalizeNamespace((namespace, actions) => {
  const res = {};
  normalizeMap(actions).forEach(({ key, val }) => {
    res[key] = function mappedAction(...args) {
      // get dispatch function from store
      let dispatch = this.$store.dispatch;
      if (namespace) {
        const 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;
});

mapActions的逻辑和mapMutations完全一样,这样就不多赘述了。

createNamespacedHelpers#

/**
 * 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 还暴露出一个createNamespacedHelpers函数,用来绑定命名空间模块namespace的值,这样后面使用时所有的map都是这个模块下面的值。

参考资料#