深入解析VueX 源码

138 阅读6分钟

前言

再上一篇文章《深入浅出VueX》中,我们简单的实现了一个 VueX ,并能够正常使用,但是没有实现其模块化思想,另外,命名空间、辅助函数、数据持久化的插件都没有实现,今天,我们来重新编写我们的源代码,实现一个功能更丰富的VueX 实现全局数据管理。

模块化的实现方式

实现模块化需要用到多叉树的数据结构,我们通过一个例子来说明这个:

let store = new Vuex.Store({
  state: { count: localStorage.getItem("qqq") / 1 || 100 },

  getters: {
    myAge(state) {
      return state.count + 20;
    },
  },

  mutations: {
    ...
  },

  actions: {
    ...
  },

  modules: {
    a: {
      state: { age: 200 },
      mutations: {
        ...
      },
      modules: {
        a1: {
          namespaced: true,
          state: { age: 300 },
          mutations: {
            ...
          },
        },
      },
    },

    b: {
      namespaced: true,
      state: { age: 400 },
      mutations: {
        ...
      },
    },
  },
});

在这个store配置项中,主模块的modules包含a和b两个子模块,a子模块包含a1子模块。我们需要将它转化成以下形式,再来进行操作:

this.root = {
   _raw: '根模块',
   _children:{
       a:{
         _raw:"a模块",
         _children:{
             a1:{
                 .....
             }
         },
         state:'a的状态'
       },
       b:{
         _raw:"b模块",
         _children:{},
         state:'b的状态'
       }
   },
   state:'根模块自己的状态'
}

编写 VueX

从这里开始我们自己来实现一个 VueX 插件,这里先来标注一下整体目录结构:

├─vuex
│  ├─module # 数据的格式化 格式化成树结构
│  │  ├─module.js
│  │  └─module-collection.js
│  ├─helpers.js # 创建辅助函数 mapState、mapGetters等等
│  ├─mixin.js # 将根组件中注入的store 分派给每一个子组件
│  ├─plugin.js # 持久化等等的插件
│  ├─index.js # 引用时的入口文件
│  ├─store.js # store构造类和install方法
│  ├─util.js # forEachValue函数,遍历对象

默认我们引用VueX使用的是index.js文件,use方法默认会调用当前返回对象的install方法。所以这个文件是入口文件,核心就是导出所有写好的方法。

import { Store, install } from "./store.js"; 

// Vuex.Store  Vuex.install
export default {
  Store,
  install,
};

export * from "./helpers";
export * from "./util";

在store.js文件里实现install方法

import applyMixin from "./mixin";

// Vue.use 方法会调用插件的install方法,此方法中的参数就是Vue的构造函数
// Vue.use = function (plugin) {
//     plugin.install(this);
// }
export const install = (_Vue) => {
  // 插件的安装 Vue.use(Vuex)
  // _Vue 是Vue的构造函数
  Vue = _Vue;
  // 需要将根组件中注入的store 分派给每一个组件 (子组件) Vue.mixin
  applyMixin(Vue);
};

在 mixin.js 文件里实现 applyMixin 方法

export default function applyMixin(Vue) {
  // 父子组件的beforeCreate执行顺序
  Vue.mixin({
    // 内部会把生命周期函数 拍平成一个数组
    beforeCreate: vuexInit,
  });
}

// 组件渲染时从父=》子
function vuexInit() {
  // 给所有的组件增加$store 属性 指向我们创建的store实例
  const options = this.$options; // 获取用户所有的选项
  if (options.store) {
    // 根实例
    this.$store = options.store;
  } else if (options.parent && options.parent.$store) {
    // 儿子 或者孙子....
    this.$store = options.parent.$store;
  }
}

在util.js里面实现forEachValue方法。

// 遍历对象,传入一个回调函数对对象中的每一项进行操作
export const forEachValue = (obj, callback) => {
  Object.keys(obj).forEach((key) => callback(obj[key], key));
};

在store.js文件里实现 Store 构造函数

import applyMixin from "./mixin";
import { forEachValue } from "./util";
import ModuleCollection from "./module/module-collection";

export let Vue;

// 容器的初始化
export class Store {
  constructor(options) {
    // options 就是你new Vuex.Store({state,mutation,actions})
    const state = options.state; // 数据变化要更新视图 (vue的核心逻辑依赖收集)
    this._mutations = {};
    this._actions = {};
    this._wrappedGetters = {};
    this._subscribes = [];
    this.getters = null;
    this._vm = null;
    this.commit = null;
    this.dispatch = null;

    // 数据的格式化 格式化成我想要的结果 (树)
    // 1.模块收集
    this._modules = new ModuleCollection(options);
    // 根模块的状态中 要将子模块通过模块名 定义在根模块上

    // 2.安装模块
    installModule(this, state, [], this._modules.root);

    // 3,将状态和getters 都定义在当前的vm上
    resetStoreVM(this, state);

    // 插件内部会依次执行
    options.plugins && options.plugins.forEach((plugin) => plugin(this));

    // 实现commit方法
    this.commit = (type, payload) => {
      //保证当前this 当前store实例
      // 调用commit其实就是去找 刚才绑定的好的mutation
      this._mutations[type].forEach((mutation) => mutation(payload));
    };

    // 实现dispatch方法
    this.dispatch = (type, payload) => {
      this._actions[type].forEach((action) => action.call(this, payload));
    };
  }

  replaceState(state) {
    // 替换掉最新的状态
    this._vm._data.$$state = state;
  }

  subscribe(fn) {
    this._subscribes.push(fn);
  }

  get state() {
    // 属性访问器   new Store().state  Object.defineProperty({get()})
    return this._vm._data.$$state;
  }
}

在module目录下的module-collection.js里面进行数据的格式化。

import { forEachValue } from "../util";
import Module from "./module";

class ModuleCollection {
  constructor(options) {
    this.root = null;
    this.register([], options);
  }

  register(path, rootModule) {
    // [a,   c]   [b]
    let newModule = new Module(rootModule);

    if (!this.root) {
      // 根模块
      this.root = newModule; // this.root就是树根
      console.log(this.root);
    } else {
      // reduce循环递归拿到上级目录,并把当前目录作为子节点放置到上级目录的_children属性上
      // 不过要截取数组最后一项,因为要根据最后一项赋值。
      let parent = path.slice(0, -1).reduce((memo, current) => {
        return memo.getChild(current);
      }, this.root);
      // 通过forEach遍历也一样。
      // let parent = this.root;
      // path.slice(0, -1).forEach((item) => {
      //   parent = parent.getChild(item);
      // });
      parent.addChild(path[path.length - 1], newModule);
    }

    if (rootModule.modules) {
      // 循环模块 [a] [b]
      forEachValue(rootModule.modules, (module, moduleName) => {
        // [a,a1]
        this.register(path.concat(moduleName), module);
      });
    }
  }

  getNamespaced(path) {
    let root = this.root; // 从根模块找起来

    return path.reduce((str, key) => {
      // [a,a1]
      root = root.getChild(key); // 不停的去找当前的模块
      return str + (root.namespaced ? key + "/" : "");
    }, ""); // 参数就是一个字符串
  }
}

export default ModuleCollection;

在module目录下的module.js抽离模块类

import { forEachValue } from "../util";

class Module {
  constructor(newModule) {
    this._raw = newModule;
    this._children = {};
    this.state = newModule.state;
  }

  getChild(key) {
    return this._children[key];
  }

  addChild(key, module) {
    this._children[key] = module;
  }

  // 给模块继续扩展方法
  forEachMutation(fn) {
    if (this._raw.mutations) {
      forEachValue(this._raw.mutations, fn);
    }
  }

  forEachAction(fn) {
    if (this._raw.actions) {
      forEachValue(this._raw.actions, fn);
    }
  }

  forEachGetter(fn) {
    if (this._raw.getters) {
      forEachValue(this._raw.getters, fn);
    }
  }

  forEachChild(fn) {
    forEachValue(this._children, fn);
  }

  get namespaced() {
    return this._raw.namespaced;
  }
}

export default Module;

在store.js里面完成installModule方法用以安装模块

通过在Module模块类中提供遍历方法,例如forEachMutation、forEachAction...对模块进行安装操作

/**
 * @param store  容器
 * @param rootState 根模块
 * @param path  所有路径
 * @param module 我们刚刚格式化后的结果
 */
const installModule = (store, rootState, path, module) => {
  // 这里我要对当前模块进行操作

  // 将所有的子模块的状态安装到父模块的状态上
  // 给当前订阅的事件 增加一个命名空间
  let namespace = store._modules.getNamespaced(path); // 返回前缀即可
  // a/changeAge  b/changeAge   a/c/changeAge   path[a] [b] [a,c]

  if (path.length > 0) {
    // vuex 可以动态的添加模块
    let parent = path.slice(0, -1).reduce((memo, current) => {
      return memo[current];
    }, rootState);

    // 如果这个对象本身不是响应式的 那么Vue.set 就相当于obj[属性 ]= 值
    Vue.set(parent, path[path.length - 1], module.state);
  }

  module.forEachMutation((mutation, key) => {
    store._mutations[namespace + key] = store._mutations[namespace + key] || [];

    store._mutations[namespace + key].push((payload) => {
      mutation.call(store, getState(store, path), payload);

      store._subscribes.forEach((fn) => {
        fn(mutation, store.state); // 用最新的状态
      });
    });
  });

  module.forEachAction((action, key) => {
    store._actions[namespace + key] = store._actions[namespace + key] || [];
    store._actions[namespace + key].push((payload) => {
      action.call(store, store, payload);
    });
  });

  module.forEachGetter((getter, key) => {
    // 模块中getter的名字重复了会覆盖
    store._wrappedGetters[namespace + key] = function() {
      return getter(getState(store, path));
    };
  });

  module.forEachChild((child, key) => {
    // 递归加载模块
    installModule(store, rootState, path.concat(key), child);
  });
};

在Store构造函数内部完成commit和dispatch方法

// 实现commit方法
this.commit = (type, payload) => {
  //保证当前this 当前store实例
  // 调用commit其实就是去找 刚才绑定的好的mutation
  this._mutations[type].forEach((mutation) => mutation(payload));
};

// 实现dispatch方法
this.dispatch = (type, payload) => {
  this._actions[type].forEach((action) => action.call(this, payload));
};

通过resetStoreVM方法定义状态和计算属性

通过“resetStoreVM(this, state)”将状态和getters 都定义在当前的vm上

function resetStoreVM(store, state) {
  const computed = {}; // 定义计算属性
  store.getters = {}; // 定义store中的getters

  forEachValue(store._wrappedGetters, (fn, key) => {
    computed[key] = () => {
      return fn();
    };

    Object.defineProperty(store.getters, key, {
      get: () => store._vm[key], // 去计算属性中取值
    });
  });

  store._vm = new Vue({
    data: {
      $$state: state,
    },
    computed, // 计算属性有缓存效果
  });
}

命名空间的实现

在ModuleCollection和Module构造函数里面实现命名空间:

// ModuleCollection:
getNamespaced(path) {
  let root = this.root; // 从根模块找起来

  return path.reduce((str, key) => {
    // [a,a1]
    root = root.getChild(key); // 不停的去找当前的模块
    return str + (root.namespaced ? key + "/" : "");
  }, ""); // 参数就是一个字符串
}

//  Module
get namespaced() {
  return !!this._raw.namespaced;
}

在installModule方法里面绑定属性,增加命名空间即可,例如:

module.forEachMutation((mutation, key) => {
    store._mutations[namespace + key] = (store._mutations[namespace + key] || []);
    store._mutations[namespace + key].push((payload) => {
        mutation.call(store, module.state, payload);
    });
});

插件

在plugins.js里面实现持久化插件

export function persists() {
  return function(store) {
    // store是当前默认传递的
    let data = localStorage.getItem("VUEX:STATE");

    if (data) {
      store.replaceState(JSON.parse(data));
    }

    store.subscribe((mutation, state) => {
      localStorage.setItem("VUEX:STATE", JSON.stringify(state));
    });
  };
}

在Store类里面实现subscribe、replaceState方法

// 造constructor函数里面执行插件
options.plugins && options.plugins.forEach((plugin) => plugin(this));

// 实现两个插件
replaceState(state) {
  // 替换掉最新的状态
  this._vm._data.$$state = state;
}

subscribe(fn) {
  this._subscribes.push(fn);
}

在store.js里面实现getState方法

function getState(store, path) {
  // 获取最新的状态 可以保证视图更新
  return path.reduce((newState, current) => {
    return newState[current];
  }, store.state);
}

获取最新状态

module.forEachMutation((mutation, key) => {
  store._mutations[namespace + key] = store._mutations[namespace + key] || [];

  store._mutations[namespace + key].push((payload) => {
    mutation.call(store, getState(store, path), payload);

    store._subscribes.forEach((fn) => {
      fn(mutation, store.state); // 用最新的状态
    });
  });
});

辅助函数

在helper.js实现辅助函数

export function mapState(stateArr) {
  let obj = {};
  for (let i = 0; i < stateArr.length; i++) {
    let stateName = stateArr[i];

    obj[stateName] = function() {
      return this.$store.state[stateName];
    };
  }
  return obj;
}

export function mapGetters(gettersArr) {
  let obj = {};
  for (let i = 0; i < gettersArr.length; i++) {
    let getterName = gettersArr[i];
    obj[getterName] = function() {
      return this.$store.getters[getterName];
    };
  }
  return obj;
}

export function mapMutations(mutationList) {
  let obj = {};
  for (let i = 0; i < mutationList.length; i++) {
    let type = mutationList[i];
    obj[type] = function(payload) {
      this.$store.commit(type, payload);
    };
  }
  return obj;
}

export function mapActions(actionList) {
  let obj = {};
  for (let i = 0; i < actionList.length; i++) {
    let type = actionList[i];
    obj[type] = function(payload) {
      this.$store.dispatch(type, payload);
    };
  }
  return obj;
}

简单使用

store/index.js配置:

import Vue from "vue";
// import Vuex from "vuex";
import Vuex, { persists } from "../vuex/index.js";

Vue.use(Vuex);

let store = new Vuex.Store({
  state: { count: localStorage.getItem("qqq") / 1 || 100 },

  plugins: [
    // logger(),  // vuex-persists
    persists(),
  ],

  getters: {
    myAge(state) {
      return state.count + 20;
    },
  },

  mutations: {
    syncChange(state, payload) {
      state.count += payload;
      localStorage.setItem("qqq", state.count);
    },
  },

  actions: {
    changeCountFn(store, payload) {
      setTimeout(() => {
        store.commit("syncChange", payload);
      }, 500);
    },
  },

  modules: {
    a: {
      state: { age: 200 },
      mutations: {
        syncChange(state, payload) {
          state.age = state.age + payload;
          console.log(state.age);
        },
      },
      modules: {
        a1: {
          namespaced: true,
          state: { age: 300 },
          mutations: {
            syncChange(state, payload) {
              state.age = state.age + payload;
              console.log(state.age + payload);
            },
          },
        },
      },
    },

    b: {
      namespaced: true,
      state: { age: 400 },
      mutations: {
        syncChange(state, payload) {
          state.age = state.age + payload;
          console.log(state.age);
        },
      },
    },
  },
});

export default store;

bro1.vue配置:

<template>
  <div class="bro1">
    <h2>当前数字是{{count}}</h2>
    <h2>当前getters是{{myAge}}</h2>
    <h2>当前a模块是{{$store.state.a.age}}</h2>
    <h2>当前a1模块是{{$store.state.a.a1.age}}</h2>
    <h2>当前b模块是{{$store.state.b.age}}</h2>
  </div>
</template>

<script>
import { mapState, mapGetters } from "../vuex";

export default {
  name: "bro1",
  methods: {},
  computed: {
    // count() {
    //   return this.$store.state.count;
    // },
    // myAge() {
    //   return this.$store.getters.myAge;
    // },
    ...mapState(["count"]),
    ...mapGetters(["myAge"]),
  },
};
</script>

<style lang="less" scoped>
.bro1 {
  border: 1px solid #8616ee;
}
</style>

bro2.vue配置:

<template>
  <div>
    <h2>当前数字是{{$store.state.count}}</h2>
    <button @click="add">+1</button>
    <button @click="minus">-1</button>
  </div>
</template>

<script>
export default {
  name: "bro2",
  methods: {
    add() {
      this.$store.dispatch("changeCountFn", 1);
      this.$store.commit("a1/syncChange", 1);
    },
    minus() {
      this.$store.commit("syncChange", -1);
      this.$store.commit("a1/syncChange", -1);
    },
  },
  mounted() {
    console.log(this.$store);
  },
};
</script>

App.vue配置:

<template>
  <div id="myApp">
    <h1>{{name}}</h1>
    <bro1></bro1>
    <bro2></bro2>
  </div>
</template>

总结

VueX源码解析完毕,通过VueX 源码,我们可以更清楚的知道VueX的工作流程。方便工作和维护。