分析 150 行完成 80%的 Vuex 功能

276 阅读4分钟

掘金上有一篇用 150 行代码实现 Vuex 80%的功能这篇文章,我非常好奇。

拜读之后,萌发了一个重新造一个“轮子”的想法,最后没想到,实现起来非常简单。

那么就写了这么一篇文章,来分析分析

Store是一个仓库,而且是一个容器,并且包含应用中的绝大部分state,就是状态。

考虑下面的代码:

State

import Vuex from 'vuex';
const store = new Vuex.Store({
  state: {
    counter: 1
  }
});

Store是一个class,负责这个整个状态的变更。

那么我们可以将实例化的Store直接实例化后,可以使用state

class Store {
  constructor(options) {
    this.options = options;
  }

  get state() {
    return this.options.state;
  }
}

const store = new Store({
  state: {
    counter: 1
  }
});
console.log(store.state); // counter

不过在此之前,我们需要将他初始化到Vue上面去。

/**
 * @description vuex初始化
 * */
function vuexInit() {
  const options = this.$options;
  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;
  }
}

接着在Store这个类中,添加这样的代码:

class Store {
+  constructor(options,Vue) {
+   Vue.mixin({beforeCreate:vuexInit});
    this.options = options;
+  }

  get state() {
    return this.options.state;
  }
}

const store = new Store({
  state: {
    counter: 1
  }
});

这样就可以直接使用 Vue 的$store 了:

const __vm__ = new Vue({
  render: h => h(App)
});
console.log(__vm__.$store.counter);

接下来,开始用mutaions来改变状态,首先需要做的是注册mutations

注册mutaions做了两件事:

  1. 拦截mutations的函数
  2. mutations里的函数传入作用域。
import { forEach } from 'lodash';

function registerMutations(store, mutationName, mutationFn) {
  store.mutations[mutationNmae] = () => {
    mutationFn.call(store, store.state);
  };
}

然后,遍历整个mutations

forEach(store.mutations, (mutationFn, mutationName) => {
  // 这里的this是指整个Store
  registerMutations(this, mutationName, mutationsFn);
});

这样就可以修改state了,但是还有一个问题是,它无法变成响应式的。

那么我们可以直接使用 Vue,将其变成响应式的。

class Store {
  constructor(options, Vue) {
    this.__vm__ = new Vue({
      data: {
        state: options.state
      }
    });
  }
  get state() {
    return this.__vm__.data.state;
  }
}

有了mutations,那么我们就需要提交mutation,而且commit及其好写:

class Store {
  constructor(options = {}, Vue) {
    const { commit } = this;
    // 让commit绑定到this上面
    this.commit = type => commit.call(this, type);
    this.mutations = {};
  }
  commit(type) {
    this.mutations[type]();
  }
}

那么完整的Store代码就是:

class Store {
  constructor(options = {}, Vue) {
    Vue.mixin({ beforeCreate: vuexInit });
    this.options = options;
    this.mutations = {};
    const { dispatch, commit } = this;
    this.commit = type => {
      return commit.call(this, type);
    };
    forEach(options.mutations, (mutationFn, mutationName) => {
      registerMutation(this, mutationName, mutationFn);
    });

    this.__vm__ = new Vue({
      data: {
        state: options.state
      }
    });
  }

  get state() {
    return this.__vm__.data.state;
  }
  commit(type) {
    this.mutations[type]();
  }
}

function vuexInit() {
  const options = this.$options;
  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;
  }
}

Actions

Vuex 的官方定义是:

  1. Action 提交的是 mutation,而不是直接变更状态
  2. Action 可以包含任何的异步操作

所以 action 里面的操作是 commit,而非变更状态:

它的用法是:

new Vuex.Store({
  state: {
    counter: 1
  },
  mutations: {
    addCounter(state, payload) {
      state.counter += payload;
    }
  },
  actions: {
    async addCounter({ commit }) {
      const value = await Axios('localhost:8080');
      commit('addCounter', value);
    }
  }
});

它和Mutation的注册相似,且call入的值类似于:

function registerAction(store, actionName, actionFn) {
  store.actions[actionName] = () => {
    actionFn.call(store, store);
  };
}

接着,遍历所有 actions,并注册:

forEach(store, (actionsFn, acrionsName) => {
  registerAction(this, actionsName, actionsFn);
});

Actions 的分发和mutation类似:

import { forEach } from 'lodash';
class Store {
  constructor(options = {}) {
    this.options = options;
    const { dispatch } = this;
    this.actions = [];
    this.dispatch = type => dispatch.call(type);
    forEach(store, (actionsFn, acrionsName) => {
      registerAction(this, actionsName, actionsFn);
    });
  }
  dispatch(type) {
    return this.actions[type]();
  }
}
function registerAction(store, actionName, actionFn) {
  store.actions[actionName] = () => {
    actionFn.call(store, store);
  };
}

然后结合mutationscommit,完成一次异步更新,代码如下:

import { forEach } from 'lodash';

export default class Store {
  constructor(options = {}, Vue) {
    Vue.mixin({ beforeCreate: vuexInit });
    this.options = options;
    this.getters = {};
    this.mutations = {};
    this.actions = {};
    const { dispatch, commit } = this;
    this.commit = type => {
      return commit.call(this, type);
    };
    this.dispatch = type => {
      return dispatch.call(this, type);
    };
    forEach(options.actions, (actionFn, actionName) => {
      registerAction(this, actionName, actionFn);
    });

    forEach(options.mutations, (mutationFn, mutationName) => {
      registerMutation(this, mutationName, mutationFn);
    });

    this.__vm__ = new Vue({
      data: {
        state: options.state
      }
    });
  }

  get state() {
    return this.options.state;
  }
  commit(type) {
    this.mutations[type]();
  }
  dispatch(type) {
    return this.actions[type]();
  }
}

function registerMutation(store, mutationName, mutationFn) {
  store.mutations[mutationName] = () => {
    mutationFn.call(store, store.state);
  };
}

function registerAction(store, actionName, actionFn) {
  store.actions[actionName] = () => {
    actionFn.call(store, store);
  };
}

function vuexInit() {
  const options = this.$options;
  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;
  }
}

这样,就可以直接使用异步更改状态了。

import Vue from 'vue';
const store = new Vuex.Store(
  {
    state: {
      counter: 1
    },
    mutations: {
      updateCounter(state) {
        state.counter++;
      }
    },
    actions: {
      updateCounterAsync({ commit }) {
        setTimeout(function() {
          commit('updateCounter');
        }, 1000);
      }
    }
  },
  Vue
);

const __store__ = new Vue({
  store
}).$mount('.app');

Getter

Geetter 可以看做为Vuex中的computed,带有一种依赖的关系的方法。

首先,写一个注册函数。因为它和MutationsActions一样,是一个函数。

function registerGetter(store, getterName, getterFn) {
  Object.defineProperty(store.getters, getterName, {
    get() {
      return getterFn(store.state);
    }
  });
}

接着遍历所有的getter,让它变成重新构造一遍方法即可。

import { forEach } from 'lodash';
class Store {
  constructor(options = {}) {
    Vue.mixin({ beforeCreate: vuexInit });
    this.options = options;
    this.getters = {};
    forEach(options.getters, (getterFn, getterName) => {
      registerGetter(this, getterName, getterFn);
    });
  }
}
function registerGetter(store, getterName, getterFn) {
  Object.defineProperty(store.getters, getterName, {
    get() {
      return getterFn(store.state);
    }
  });
}

module

这是最复杂的一部分,相当于将其重写了一遍。

首先,我们先定义一个 ModuleCollection 类,然后将模块定义的方法:

class ModuleCollection {
  constructor(rawRootModule) {
    this.register([], rawRootModule);
  }
  register(path, _rawModule) {
    const newModule = {
      _children: {}, // 嵌入或者递归到_children上面去,
      _rawModule, // 展开递归
      state: _rawModule.state
    };
    if (path.length === 0) {
      // 如果模块的key数组长度为0的话,直接赋值给root
      this.root = newModule;
    } else {
      // 如果不是,那么就直接找到module的值,然后降级到最后一个值
      const parent = path.slice(0, -1).reduce((module, key) => {
        return module._children(key);
      }, this.root);

      // 最后一个path为新模块
      parent._children[path[path.length - 1]] = newModule;
    }

    // 直接遍历所有module,然后注册为模块
    if (_rawModule.module) {
      forEach(_rawModule.modules, (rawChildModule, key) => {
        this.register(path.concat(key), rawChildModule);
      });
    }
  }
}

既然找出了所有可能存在module的状态,那么剩下就是将所有的module全部变成可以操作store

那么 installceModule 函数则是:

function installModule(store, rootState, path, module) {
  if (path.length > 0) {
    const getLastModule = path[path.length - 1];
    // 这是rootState,然后重新设置为响应式的方式
    _Vue.set(rootState, getLastModule, module.state);
  }
  const context = {
    dispatch: store.dispatch,
    commit: store.commit
  };

  const local = Object.defineProperties(context, {
    getters: {
      get: () => store.getters
    },
    state: {
      get: () => {
        let state = store.state;
        return path.length
          ? path.reduce((state, key) => state[key], state)
          : state;
      }
    }
  });

  if (module._rawModule.actions) {
    forEach(module._rawModule.actions, (actionFn, actionName) => {
      registerAction(store, actionName, actionFn, local);
    });
  }
  if (module._rawModule.getters) {
    forEach(module._rawModule.getters, (getterFn, getterName) => {
      registerGetter(store, getterName, getterFn, local);
    });
  }
  if (module._rawModule.mutations) {
    forEach(module._rawModule.mutations, (mutationFn, mutationName) => {
      registerMutation(store, mutationName, mutationFn, local);
    });
  }
  // 递归注册所有的modules
  forEach(module._children, (child, key) => {
    installModule(store, rootState, path.concat(key), child);
  });
}

最后Vuex的源码为:

let _Vue;
export class Store {
  constructor(options = {}, Vue) {
    _Vue = Vue;
    Vue.mixin({ beforeCreate: vuexInit });
    this.getters = {};
    this._mutations = {}; // 在私有属性前加_
    this._wrappedGetters = {};
    this._actions = {};
    this._modules = new ModuleCollection(options);
    const { dispatch, commit } = this;
    this.commit = type => {
      return commit.call(this, type);
    };
    this.dispatch = type => {
      return dispatch.call(this, type);
    };
    const state = options.state;
    const path = []; // 初始路径给根路径为空
    installModule(this, state, path, this._modules.root);
    this._vm = new Vue({
      data: {
        state: state
      }
    });
  }

  get state() {
    // return this.options.state; // 无法完成页面中的双向绑定,所以改用this._vm的形式
    return this._vm._data.state;
  }
  commit(type) {
    this._mutations[type].forEach(handler => handler());
  }
  dispatch(type) {
    return this._actions[type][0]();
  }
}

class ModuleCollection {
  constructor(rawRootModule) {
    this.register([], rawRootModule);
  }
  register(path, rawModule) {
    const newModule = {
      _children: {},
      _rawModule: rawModule,
      state: rawModule.state
    };
    if (path.length === 0) {
      this.root = newModule;
    } else {
      const parent = path.slice(0, -1).reduce((module, key) => {
        return module._children(key);
      }, this.root);
      parent._children[path[path.length - 1]] = newModule;
    }
    if (rawModule.modules) {
      forEachValue(rawModule.modules, (rawChildModule, key) => {
        this.register(path.concat(key), rawChildModule);
      });
    }
  }
}

function installModule(store, rootState, path, module) {
  if (path.length > 0) {
    const parentState = rootState;
    const moduleName = path[path.length - 1];
    _Vue.set(parentState, moduleName, module.state);
  }
  const context = {
    dispatch: store.dispatch,
    commit: store.commit
  };
  const local = Object.defineProperties(context, {
    getters: {
      get: () => store.getters
    },
    state: {
      get: () => {
        let state = store.state;
        return path.length
          ? path.reduce((state, key) => state[key], state)
          : state;
      }
    }
  });
  if (module._rawModule.actions) {
    forEachValue(module._rawModule.actions, (actionFn, actionName) => {
      registerAction(store, actionName, actionFn, local);
    });
  }
  if (module._rawModule.getters) {
    forEachValue(module._rawModule.getters, (getterFn, getterName) => {
      registerGetter(store, getterName, getterFn, local);
    });
  }
  if (module._rawModule.mutations) {
    forEachValue(module._rawModule.mutations, (mutationFn, mutationName) => {
      registerMutation(store, mutationName, mutationFn, local);
    });
  }
  forEachValue(module._children, (child, key) => {
    installModule(store, rootState, path.concat(key), child);
  });
}

function registerMutation(store, mutationName, mutationFn, local) {
  const entry =
    store._mutations[mutationName] || (store._mutations[mutationName] = []);
  entry.push(() => {
    mutationFn.call(store, local.state);
  });
}

function registerAction(store, actionName, actionFn, local) {
  const entry = store._actions[actionName] || (store._actions[actionName] = []);
  entry.push(() => {
    return actionFn.call(store, {
      commit: local.commit,
      state: local.state
    });
  });
}

function registerGetter(store, getterName, getterFn, local) {
  Object.defineProperty(store.getters, getterName, {
    get: () => {
      return getterFn(local.state, local.getters, store.state);
    }
  });
}

// 将对象中的每一个值放入到传入的函数中作为参数执行
function forEachValue(obj, fn) {
  Object.keys(obj).forEach(key => fn(obj[key], key));
}

function vuexInit() {
  const options = this.$options;
  if (options.store) {
    // 组件内部设定了store,则优先使用组件内部的store
    this.$store =
      typeof options.store === 'function' ? options.store() : options.store;
  } else if (options.parent && options.parent.$store) {
    // 组件内部没有设定store,则从根App.vue下继承$store方法
    this.$store = options.parent.$store;
  }
}

缺点

  1. 它无法实现传入载荷进函数内
  2. 没有mapStatesmapMutationsmapActions
  3. 没有namespace