A brief introduction.

Vuex is a state management mode developed specifically for vue.js applications. It uses centralized storage to manage the state of all components of an application and rules to ensure that the state changes in a predictable way.

  • Change state with unique entry mutation

  • Vuex cannot be used alone depending on VUE

Let’s implement a Vuex by ourselves.

Two. Vuex module implementation

1. Install method

When the install method is called, passing in the constructor of the current Vue, I save the \_Vue passed in in the Vue variable and export it so that the Vue I am currently using is the same as the Vue I wrote the project to

The ES6 module outputs references to values. When the JS engine statically analyzes a script, it generates a read-only reference to the module load command import. When the script is actually executed, it will be evaluated in the loaded module based on the read-only reference.

let Vue;
export class Store {
    constructor(options){}}export const install = (_Vue) = >{
    Vue = _Vue;
    / /...
}
Copy the code

2. A mixin method

Qs: How do all components have access to store objects?

Mixin ({beforeCreate}), take store, mount it to itself, take the root component’s store, and share it with each component

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; }}}});Copy the code

3. The state

Qs: Change store. State, how to notify view update? Create a vue instance and put store.state on data. The property accessor accesses $store.state proxy to this._vm.$$state

Defining the data passed in by the user on an instance of VUE (this is the VUex core) generates a separate instance of VUE for communication, noting that variables that define the beginning of $are not proxied on the instance

import { Vue } from "./install";

class Store {
  constructor(options) {
    // The $store used in the user component is equivalent to 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;
Copy the code

Because we’re going to iterate multiple times we’re going to write a traversal method over here just to make it easier

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

4. Implement getters

Getters in VUex is equivalent to computed in Vue, which does not run by default. Define the data on getters to computed and delegate the value of getters to 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 // Cache with calculated attributes
});


Copy the code

5. Implement mutations

Commit calls the muations method by iterating the user’s incoming muations to the Store 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); }}Copy the code

6. Implement the actions

Dispatch calls the actions method by iterating the assigned actions from the user to the Store’s 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); }}Copy the code

Three. Implementation of the module mechanism

NameSpaced can resolve the naming conflicts between child and parent modules, adding a namespace

1. Format user data options

The corresponding moudle is placed under the corresponding children and formatted as follows to register the module parent-child relationship using a tree structure

this.root={
  _raw: user-defined module,state: State of the current module itself,_children: {a: {_raw: user-defined module,state: State of the current module itself,_children: {// List of submodules}}}}Copy the code

Path. length=0, register as the root module and recursively register modules for the root module

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); }); }}}Copy the code

2. Remove the module class

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; }}Copy the code

3. Install the module

When there is no namespace, traverse the formatted data and put getters on the root, merge acrions into an array

this._actions = {};
this._mutations = {};
this._wrappedGetters = {};
// Install the module
installModule(this, state, [], this._modules.root);
Copy the code

Provide traversal methods in module classes

 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);
  }
Copy the code

Install the module

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;
    // Adding attributes directly to the object does not cause the view to be updated, via 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);
  });
}
Copy the code

Rewrite the Dispatch and Action methods

 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));
  };
Copy the code

Define state and 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
    });
}
Copy the code

5. Implement namespaces

NameSpaced can resolve the naming conflicts between child and parent modules, adding a namespace

Default getters are defined to the parent module if nameSpaced is not present

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; }}Copy the code

Add a namespace to the binding property

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);
  });
}

Copy the code

6. Register modules

To register a module, register the current module with _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);
  }
Copy the code

Reset the instance

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) {
    // Destroy the last instance created
    Vue.nextTick(() = >oldVm.$destroy()); }}Copy the code

4. Realize plug-in function

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

1. Realize the logger

Vuex comes with a Logger plug-in. It’s super easy to write a logger plug-in by hand

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);
    });
  };
}
Copy the code

2. Implement persistent plug-ins

Refresh the data and re-initialize the vuex. Now let’s implement a plugin on our own

function persists() {
  return function(store) {
    let localState = JSON.parse(localStorage.getItem("VUEX:STATE"));
    if (localState) {
      store.replaceState(localState);
    }
    store.subscribe((mutation, rootState) = > {
      // You need to do a throttle-lodash
      localStorage.setItem("VUEX:STATE".JSON.stringify(rootState));
    });
  };
}
Copy the code

3. Implement plugins, subscribe, and 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)
      );

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

Copy the code

4. Obtain the latest status

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)
      );
    });
  });
}
Copy the code

5. Auxiliary functions

1. The mapState implementation

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

2. MapGetters implementation

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

3. MapMutations implementation

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

4. MapActions implementation

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

Mutation and action

Strict modestrict: true, failed to passmutationModify thestateWhen,vuexThe following error message is displayed

“We do this internally by maintaining a variable (_research) to determine if this is happening in mutaion

 _withCommitting(fn) {
    this._committing = true;
    fn(); (" this function is synchronous (true), (" get _research (false) if fn is asynchronous (")
    this._committing = false;
  }
Copy the code

Added synchronous Watcher in strict mode to monitor state changes

if (store.strict) {
    store._vm.$watch(
      () = > store._vm._data.$$state,
      () = > {
        console.assert(
          store._committing,
          "' State changed outside mutation '"
        );
      },
      { deep: true.sync: true}); }Copy the code

Internal change status is a normal update, so you need to wrap this with _withresearch too

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