深入Vuex原理:实现一个简易Vuex

566 阅读2分钟

一.简介

Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式。它采用集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化。

  • 通过唯一的入口mutation 更改 state

  • Vuex 不能单独使用 依赖 vue 下面让我们自己动手实现一个vuex吧。

二.实现 Vuex 模块

1.install 方法

当调用install方法的时候,传入当前版本Vue的构造函数,我把传入的\_Vue 保存在 Vue 变量,并且把它导出 这样的话,我当前用的 vue 和写项目的 vue 版本一致

ES6 模块输出的是值的引用。JS 引擎对脚本静态分析的时候,遇到模块加载命令 import,就会生成一个只读引用。等到脚本真正执行时,再根据这个只读引用,到被加载的那个模块里面去取值。

let Vue;
export class Store {
    constructor(options){}
}

export const install = (_Vue) =>{
    Vue = _Vue;
    //...
}

2.mixin 方法

qs:怎么让所有组件都能访问到store对象?

所有组件都能执行的方法,Vue.mixin({beforeCreate}),拿到 store 挂载到自己的身上 拿到根组件的 store,将它共享给每个组件

Vue.mixin({
    beforeCreate() {
      let options = this.$options;
      if (options.store) {
        this.$store = options.store;
      } else {
        if (this.$parent && this.$parent.$store) {
          this.$store = this.$parent.$store;
        }
      }
    },
  });

3.实现 state

qs:改变 store.state,怎么通知视图更新? 创建一个 vue 实例 把 store.state 放到 data 上 属性访问器 访问$store.state代理到this._vm.$$state

将用户传入的数据定义在 vue 的实例上 (这个就是 vuex 核心)产生一个单独的 vue 实例进行通信,这里要注意的是定义$开头的变量不会被代理到实例上

import { Vue } from "./install";

class Store {
  constructor(options) {
    //用户组件中使用的$store 等价于这里的this
    let { state, mutation, actions, moudle, strict, getters } = options;

    this._vm = new Vue({
      data: {
        $$state: state,
      },
    });
  }
  get state(){
      return this._vm._data.$$state
  }
}

export default Store;

因为下面用多次遍历 所以我们在这边写一个遍历的方法,方便后续操作

export const _forEach = (obj, fn) => {
  Object.keys(obj).forEach((key) => {
    fn(obj[key], key);
  });
};

4.实现 getters

vuex 里的 getters 就相当于 vue 的 computed 默认不执行,取值才执行,具有缓存。 把 getters 上的数据定义到 computed 上 同时对 把 getters 取值代理到 computed 上

this.getters = {};
const computed = {}
_forEach(options.getters, (fn, key) => {
    computed[key] = () => {
        return fn(this.state);
    }
    Object.defineProperty(this.getters,key,{
        get:()=> this._vm[key]
    })
});
this._vm = new Vue({
    data: {
        $$state: state,
    },
    computed // 利用计算属性实现缓存
});


5.实现 mutations

把用户传入的 muations 遍历赋值到 Storemuationscommit 调用 muations 的方法

export class Store {
    constructor(options) {
        this.mutations = {};
        _forEach(options.mutations, (fn, key) => {
            this.mutations[key] = (payload) => fn.call(this, this.state, payload)
        });
    }
    commit = (type, payload) => {
        this.mutations[type](payload);
    }
}

6.实现 actions

把用户传入的 actions 遍历赋值到Storeactions dispatch 调用 actions 的方法

export class Store {
    constructor(options) {
        this.actions = {};
        _forEach(options.actions, (fn, key) => {
            this.actions[key] = (payload) => fn.call(this, this,payload);
        });
    }
    dispatch = (type, payload) => {
        this.actions[type](payload);
    }
}

三.实现模块机制

nameSpaced 可以解决子模块和父模块的命名冲突文件,相当于增加了一个命名空间

1.格式化用户数据 options

对应的 moudle 放入对应的 children 下 格式化成如下格式 用树的结构来注册模块父子关系

this.root={
  _raw:用户定义的模块,
  state:当前模块自己的状态,
  _children:{
    a:{
      _raw:用户定义的模块,
      state:当前模块自己的状态,
      _children:{
//子模块列表
      }
    }
  }
}

path.length=0,注册为根模块 再递归注册根模块的 modules

class ModuleCollection {
  constructor(options) {
    this.root = null;
    this.register([], options);
  }
  register(path, rootModule) {
    let newModule = {
      _raw: rootModule,
      _children: {},
      state: rootModule.state,
    };
    if (path.length == 0) {
      this.root = newModule;
    } else {
      let parent = path.slice(0, -1).reduce((memo, current) => {
        return memo._children[current];
      }, this.root);
      parent._children[path[path.length - 1]] = newModule;
    }
    if (rootModule.modules) {
      forEach(rootModule.modules, (module, key) => {
        this.register(path.concat(key), module);
      });
    }
  }
}

2.抽离模块类

export default class Module {
  constructor(rawModule) {
    this._raw = rawModule;
    this._children = {};
    this.state = rawModule.state;
  }
  getChild(childName) {
    return this._children[childName];
  }
  addChild(childName, module) {
    this._children[childName] = module;
  }
}

3.安装模块

没有 namespace 的时候, 遍历格式化后的数据 把 getters 都放在根上 mutations acrions 合并成数组

this._actions = {};
this._mutations = {};
this._wrappedGetters = {};
// 安装模块
installModule(this, state, [], this._modules.root);

在模块类中提供遍历方法

 addChild(childName, module) {
    this._children[childName] = module;
  }
  forEachGetter(cb) {
    this._raw.getters && _forEach(this._raw.getters, cb);
  }
  forEachMutation(cb) {
    this._raw.mutations && _forEach(this._raw.mutations, cb);
  }
  forEachAction(cb) {
    this._raw.actions && _forEach(this._raw.actions, cb);
  }
  forEachChildren(cb) {
    this._children && _forEach(this._children, cb);
  }

对模块进行安装操作

function installModule(store, rootState, path, root) {
  if (path.length > 0) {
    let parent = path.slice(0, -1).reduce((memo, current) => {
      return memo[current];
    }, rootState);
    // parent[path[path.length - 1]] = root.state;
    //对象直接新增属性不能导致重新更新视图,要通过Vue.set
    Vue.set(parent, path[path.length - 1], root.state);
    console.log(rootState, "rootState");
  }

    root.forEachGetter((fn, key) => {
    store.wrapperGetters[key] = function() {
      return fn.call(store, root.state);
    };
  });
  root.forEachMutation((fn, key) => {
    store.mutations[key] = store.mutations[key] || [];
    store.mutations[key].push((payload) => {
      return fn.call(store, root.state, payload);
    });
  });
  root.forEachAction((fn, key) => {
    store.actions[key] = store.actions[key] || [];
    store.actions[key].push((payload) => {
      return fn.call(store, store, payload);
    });
  });
  root.forEachChildren((child, key) => {
    installModule(store, rootState, path.concat(key), child);
  });
}

dispatchaction方法进行重写

 commit = (mutationName, payload) => {
    this.mutations[mutationName] &&
      this.mutations[mutationName].forEach((fn) => fn(payload));
  };
  dispatch = (actionName, payload) => {
    this.actions[actionName] &&
      this.actions[actionName].forEach((fn) => fn(payload));
  };

4.定义 state 和 computed

function resetStoreVM(store, state) {
    const computed = {};
    store.getters = {};
    const wrappedGetters = store._wrappedGetters
    _forEach(wrappedGetters, (fn, key) => {
        computed[key] = () => {
            return fn(store.state);
        }
        Object.defineProperty(store.getters, key, {
            get: () => store._vm[key]
        })
    });
    store._vm = new Vue({
        data: {
            $$state: state,
        },
        computed
    });
}

5.实现命名空间

nameSpaced 可以解决子模块和父模块的命名冲突文件,相当于增加了一个命名空间

如果没有 nameSpaced 默认 getters 会被定义到父模块上

import { forEachValue } from '../util';
import Module from './module';
export default class ModuleCollection {
    getNamespace(path) {
        let module = this.root
        return path.reduce((namespace, key) => {
            module = module.getChild(key);
            console.log(module)
            return namespace + (module.namespaced ? key + '/' : '')
        }, '');
    }
}
export default class Module {
    get namespaced(){
        return !!this._rawModule.namespaced;
    }
}

在绑定属性是增加命名空间即可

function installModule(store, rootState, path, root) {
+ let namespace = store._modules.getNamespace(path);
  if (path.length > 0) {
    let parent = path.slice(0, -1).reduce((memo, current) => {
      return memo[current];
    }, rootState);
    Vue.set(parent, path[path.length - 1], root.state);
  }

  root.forEachGetter((fn, key) => {
+    store.wrapperGetters[namespace + key] = function() {
      return fn.call(store, root.state);
    };
  });
  root.forEachMutation((fn, key) => {
+    store.mutations[namespace + key] = store.mutations[namespace + key] || [];
+    store.mutations[namespace + key].push((payload) => {
      return fn.call(store, root.state, payload);
    });
  });
  root.forEachAction((fn, key) => {
+    store.actions[namespace + key] = store.actions[namespace + key] || [];
+    store.actions[namespace + key].push((payload) => {
      return fn.call(store, store, payload);
    });
  });
  root.forEachChildren((child, key) => {
    installModule(store, rootState, path.concat(key), child);
  });
}

6.注册模块

实现模块的注册,就是将当前模块注册到_modules 中

 registerModule(path, module) {
    if (typeof path == "string") path = [path];
    this._modules.register(path, module);
    installModule(this, this.state, path, module.newModule);
    restVM(this, this.state);
  }

重置实例

function restVM(store, state) {
  let oldVm = store._vm;
  const computed = {};
  store.getters = {};
  forEach(store.wrapperGetters, (getter, key) => {
    computed[key] = getter;
    Object.defineProperty(store.getters, key, {
      get: () => store._vm[key],
    });
  });
  store._vm = new Vue({
    data: {
      $$state: state,
    },
    computed,
  });
  if (oldVm) {
    //销毁上次创建的实例
    Vue.nextTick(() => oldVm.$destroy());
  }
}

四.实现插件功能

store = new Vuex.Store({
  plugins: [logger(), persists("localStorage")],
)}

1.实现 logger

vuex 自带 logger 插件 我们手写一个 logger 插件也超级简单的

function logger() {
  return function(store) {
    let prevState = JSON.stringify(store.state);
    store.subscribe((mutation, state) => {
      console.log("prevState:", prevState);
      console.log("mutation:", mutation);
      console.log("currentState:", JSON.stringify(state));
      prevState = JSON.stringify(state);
    });
  };
}

2.实现 persists 持久化插件

刷新数据 vuex 重新初始化,现在让我们自己来实现一个 persists 插件吧

function persists() {
  return function(store) {
    let localState = JSON.parse(localStorage.getItem("VUEX:STATE"));
    if (localState) {
      store.replaceState(localState);
    }
    store.subscribe((mutation, rootState) => {
      // 这里需要做一个节流  throttle lodash
      localStorage.setItem("VUEX:STATE", JSON.stringify(rootState));
    });
  };
}

3.实现 plugins、subscribe、replaceState

class Store {
  constructor(options) {
    this._modules = new ModuleCollection(options);
    installModule(this, state, [], this._modules.root);
   //...
+    if (options.plugins) {
      options.plugins.forEach((plugin) => plugin(this));
    }
  }
+  subscribe(fn) {
    this._subscribes.push(fn);
  }
+  replaceState(state){
	this._vm._data.$$state = state;
}
  //...
}

function installModule(store, rootState, path, root) {
  let namespace = store._modules.getNamespace(path);
  //...
  root.forEachMutation((fn, key) => {
    store.mutations[namespace + key] = store.mutations[namespace + key] || [];
    store.mutations[namespace + key].push((payload) => {
     fn.call(store, root.state, payload);
+    store._subscribes.forEach((fn) =>
        fn({ type: namespace + key, payload }, rootState)
      );

    });
  });
 //...
}

4. 获取最新状态

function getNewState(store, path) {
  return path.reduce((memo, current) => {
    return memo[current];
  }, store.state);
}

function installModule(store, rootState, path, root) {
  let namespace = store._modules.getNamespace(path);
//...
  root.forEachGetter((fn, key) => {
    store.wrapperGetters[namespace + key] = function() {
+      return fn.call(store, getNewState(store, path));
    };
  });
  root.forEachMutation((fn, key) => {
    store.mutations[namespace + key] = store.mutations[namespace + key] || [];
    store.mutations[namespace + key].push((payload) => {
+     fn.call(store, getNewState(store, path), payload);
      store._subscribes.forEach((fn) =>
        fn({ type: namespace + key, payload }, store.state)
      );
    });
  });
}

五.辅助函数

1.mapState 实现

function mapState(stateList) {
  let obj = {};
  stateList.forEach((stateName) => {
    obj[stateName] = function() {
      return this.$store.state[stateName];
    };
  });
  return obj;
}

2.mapGetters 实现

function mapGetters(gettersList) {
  let obj = {};
  gettersList.forEach((getterName) => {
    obj[getterName] = function() {
      return this.$store.getters[getterName];
    };
  });
  return obj;
}

3.mapMutations 实现

function mapMutations(mutationList) {
  let obj = {};
  mutationList.forEach((mutationName) => {
    obj[mutationName] = function(payload) {
      this.$store.commit(mutationName, payload);
    };
  });
  return obj;
}

4.mapActions 实现

function mapActions(actionList) {
  let obj = {};
  actionList.forEach((actionName) => {
    obj[actionName] = function(payload) {
      this.$store.dispatch(actionName, payload);
    };
  });
  return obj;
}

六.区分 mutation 和 action

严格模式strict: true,没有通过 mutation 修改 state 时,vuex 会提示如下错误

我们通过内部维护一个变量_committing,来判断是不是在 mutaion 中更改的

 _withCommitting(fn) {
    this._committing = true;
    fn(); //函数是同步的,获取_committing为true,如果fn是异步的,那么获取_committing为false
    this._committing = false;
  }

严格模式下增加同步 watcher,监控状态变化

if (store.strict) {
    store._vm.$watch(
      () => store._vm._data.$$state,
      () => {
        console.assert(
          store._committing,
          "'在mutation之外更改了状态'"
        );
      },
      { deep: true, sync: true }
    );
  }

内部更改状态属于正常更新,所以也需要用_withCommitting 进行包裹

store._withCommitting(() => {
  Vue.set(parent, path[path.length - 1], module.state);
})
replaceState(newState) { \
  this._withCommitting(() => {
      this._vm._data.$$state = newState;
  })
}