关于前端:奥利给从零打造MiniVuex

2次阅读

共计 9490 个字符,预计需要花费 24 分钟才能阅读完成。

通知各位一个鬼故事,Vuex 出了 Vue3 的版本啦,新的一年新的学不动,趁 Vuex 还晓得如何在 Vue2 的应用,连忙抓住它的尾巴,擦出五彩缤纷的火花,实现一个属于咱们本人的 mini Vuex 吧 (๑•̀ㅂ•́)و✧

目录构造及介绍

MiniVuex
├── applyMixin.js       提供了 store 在 Vue 实例上的注入
├── helper.js           提供辅助函数
├── index.js            主入口
├── store.js            Store 类
└── utils.js            提供工具办法

理解大略的目录和性能后咱们开始对咱们平时的操作流程别离对对应的源码进行实现。

MiniVuex.Store

MiniVuex/ index.js

先放出咱们的主入口文件,接下来咱们一一实现这些办法。

import {Store, install} from './store';

import {mapState, mapGetters, mapMutations, mapActions} from './helper';

export default {install, Store};
export {mapState, mapGetters, mapMutations, mapActions};

先联合咱们平时的应用一步步进行吧,首先是将 MiniVuex 装置到 Vue 中,并将 MiniVuex.Store 创立后返回的类注入到 Vue 的实例中。

装置 MiniVuex,并创立 store 实例

src/store.js

import Vue from 'vue'
import MiniVuex from './MiniVuex'

// install Vuex 框架
Vue.use(MiniVuex)

// 创立并导出 store 对象。为了不便,先不不配置参数
export default new MiniVuex.Store()

src/main.js

import Vue from 'vue';
import App from './App.vue';
import store from './store';

Vue.config.productionTip = false;

new Vue({
  store, // 将 store 传入 Vue 的配置项中
  render: h => h(App)
}).$mount('#app');

以上是 MiniVuex 的根本应用,接下咱们针对以上的代码进行剖析和实现。

install

首先利用 Vue.use 装置插件是会调用对应插件的 install 办法, 一下是 Vue.use 的源码。

Vue.use = function (plugin) {var installedPlugins = (this._installedPlugins || (this._installedPlugins = []));
      if (installedPlugins.indexOf(plugin) > -1) {return this}

      // additional parameters
      var args = toArray(arguments, 1);
      args.unshift(this);
      if (typeof plugin.install === 'function') {plugin.install.apply(plugin, args);
      } else if (typeof plugin === 'function') {plugin.apply(null, args);
      }
      installedPlugins.push(plugin);
      return this
    };

能够看出在如果装置插件的话,插件本身须要有有个 install 函数,利用 Vue.use 装置插件时 Vue 会执行一下插件的 install 办法并且将本身(Vue)作为参数传给插件,而后插件就能够对传入的 Vue 嘿嘿嘿 进行注入操作啦~

那么咱们开始来实现插件的 install 吧。

MiniVuex/store.js

import applyMixin from './applyMixin';

// 保留 Vue 构造函数
let Vue;

class Store {}

// 装置 Vuex
const install = _Vue => {
  // 保留 Vue 构造函数
  applyMixin((Vue = _Vue));
};

export {Store, install};

MiniVuex/applyMixin.js

// 将 Store 注入到组件中
export default function applyMixin(Vue) {
  // 检测 Vue 版本,只满足 V2 版本
  let version = Vue.version.split('.')[0];

  if (version == 2) {
    // 合乎版本
    // 将 Vuex 初始化函数混入到每个组件的 beforeCreate 生命周期中
    Vue.mixin({beforeCreate: VuexInit});
  } else {console.log(`(〒︿〒)你的版本太 ${version >= 3 ? '高' : '低'}了 `);
  }
}

// 初始化
export function VuexInit() {
  var options = this.$options;
  // 将初始化根组件的  store 设置为 组件的 $store 属性
  // 判断根组件是否有注入 store
  // 因为咱们是应用了 Vue.mixin 进行 store 注入,Vue 外部会帮我门进行递归解决,所以咱们不须要思考递归实现
  if (options.store) {this.$store = options.store;} else if (options.parent && options.parent.$store) {
    // 子组件取父组件的 $store 属性,一层一层嵌套进行设置
    this.$store = options.parent.$store;
  }
}

install 搞好后接下来就是重头戏了,这个时候就轮到咱们的 MiniVue.Store 进行状态贮存对象的创立了,如果对 Vuex 的 Vuex.Store 状态结构器所须要的参数配置不太理解的话倡议先看看 官网文档。

咱们是 Mini 版的 Vuex,所以并不会全副配置都实现,只须要实现罕用的配置项反对即可,次要反对 state, getters, mutations, actionsmodules

state

先逐个对每个配置项进行解决。
首先创立贮存对象时得要先定义好 state 对象来初始化咱们提前定义咱们 store 的状态值。

MiniVuex/utils.js

定义对象遍历函数

export const forEachObj = (obj, fn) => {for (var key in obj) {fn && fn(key, obj[key]);
  }
};

MiniVuex/store.js

import applyMixin from './applyMixin';
import {forEachObj} from './utils';

// 保留 Vue 构造函数
let Vue;

// 爱护 Vue 实例,防止被注入批改
const VM = Symbol('VM');

class Store {
  constructor({
    state,
    ...options
  }) {
    // 利用 Vue 来进行 状态监听,实现更改状态实时更改视图
    this[VM] = new Vue({
      data: {
        /**
        * 为什么不间接 {...state} 或者 {data: state} 呢?
        * 因为 state 中可能含有以 _ 或 $ 结尾的属性,而间接在 data 属性中以 _ 或 $ 结尾的属性不会被 Vue 实例代理,无奈通过 vm[property] 的形式获取,* 因为它们可能和 Vue 内置的 property、API 办法抵触。* 为了对立获取咱们就将它们设置在 data 一个属性中,前面间接拜访这个属性即可。**/
        $$state: state
      }
    });
  }

  // 能够通过 store.state 获取状态汇合,所以这里通过 getters 对 state 做监听和数据返回    
  get state() {return this[VM]._data.$$state;
  }
}

// 装置 Vuex
const install = _Vue => {
  // 保留 Vue 构造函数
  applyMixin((Vue = _Vue));
};

export {Store, install};

getters

Vuex 容许咱们在 store 中定义“getter”(能够认为是 store 的计算属性)。就像计算属性一样,getter 的返回值会依据它的依赖被缓存起来,且只有当它的依赖值产生了扭转才会被从新计算。– Vuex 文档

所以咱们也能够利用 Vue 的 computed 对咱们设置的 getters 进行监听和缓存解决即可。Let’s Coding~

//...
class Store{constructor({state,getters = {},...options}){
        //...
        
        const computed = {};
        this.getters = {};
        
        // 遍历 getters,将它的值封装成一个新的对象并且赋值给 Vue 的 computed 进行缓存
        forEachObj(getters, (key, gets) => {computed[key] = () => gets(this.state);
          
          // 确保咱们每次的取值都是最新的,对其进行监听
          Object.defineProperty(this.getters, key, {get: () => this[VM][key]
          });
        });
    
        this[VM] = new Vue({
          data: {$$state: state},
          // 将 computed 配置项传给 Vue 
          computed: computed
        });
    }
    //...
}
//...

mutations

偷个懒,贴官网的阐明(;´▽`)y-~~

更改 Vuex 的 store 中的状态的惟一办法是提交 mutation。Vuex 中的 mutation 十分相似于事件:每个 mutation 都有一个字符串的 事件类型 (type) 和 一个 回调函数 (handler)。这个回调函数就是咱们理论进行状态更改的中央,并且它会承受 state 作为第一个参数。

举个例子,先创立一个含有 mutations 的 store。

usage

const store = new Vuex.Store({
  state: {count: 1},
  mutations: {add (state,payload) {
      // 变更状态
      state.count += payload
    }
  }
})

如果 Wimbledon 想要对 count 进行更新的话,咱们须要这样做:

store.commit('add',1); // state.count 更新为 2

大抵的用法根本理解了,撒,哈子麻油~

MiniVuex/store.js

// ..

class Store{constructor({state,getters = {},mutations = {},...options}){
    //...
    
    // this 重定向
    this.commit = this.commit.bind(this);
    
    this.mutations = mutations;
    //...
    }
    
    // 提交到  mutations 进行数据更改
    commit(key, ...payload) {if(this.mutations[key] instanceof Function ){this.mutations[key](this.state, ...payload);
        }else{console.log(`[MiniVuex]: mutations 中的 ${mutations} 并不是一个办法~`)
        }
    }
    //...
}
// ..

action

Action 相似于 mutation,不同在于:

  • Action 提交的是 mutation,而不是间接变更状态。
  • Action 能够蕴含任意异步操作。

Action 函数承受一个与 store 实例具备雷同办法和属性的 context 对象,因而你能够调用 context.commit 提交一个 mutation,或者通过 context.state 和 context.getters 来获取 state 和 getters。

持续拿下面的 store 来用~

usage

const store = new Vuex.Store({
  state: {count: 1},
  mutations: {add (state,payload) {
      // 变更状态
      state.count += payload
    }
  },
  actions: {addAsync (context) {
        // 能够在 actions 中异步申请数据,待服务器响应返回数据时在通过 commit 派发 ** 一个或者多个 ** 进行状态更新
        axios.then((res) => {context.commit('add',res?.data?.count || 0)
        }).catch(err => {console.log(err)
        })
    }
  }
})

派发更新

store.dispatch('addAsync')

MiniVuex/store.js

//...
class Store{
    constructor({state,
    getters = {},
    mutations = {},
    actions = {},
    ...options}){
        // 局部代码省略
        
        // this 重定向
        this.dispatch = this.dispatch.bind(this);
        
        this.actions = actions;
        
    }
    
    // 可通过 commit 提交到 mutations 进行数据更改
    dispatch(key, ...payload) {
        // 将 Store 实例传给对应的 actions 函数
        this.actions[key](this, ...payload);
    }
    
    // 局部代码省略
}
//...

modules

模块也是 Vuex 外围的一部分,每个模块领有本人的 state、mutation、action、getter、甚至是嵌套子模块——从上至下进行同样形式的宰割。

这里我的解决形式和 Vuex 有一些不同,我是间接用一个 Map 贮存每个模块实例的,并给他们各自标记一个特定的层级方便管理或操作。

// 省略局部代码

const VM = Symbol('VM');

const nameSpace = new Map();

class Store {
  constructor({
    state,
    getters = {},
    mutations = {},
    actions = {},
    modules = {},
    ...options
  }) {
    
    // 省略局部代码
    
    //  判断 nameSpace 之前是否有贮存过实例,没有的话就是根模块
    
    if (!nameSpace.size) {
        // 标记模块 并将模块贮存到 nameSpace 中
        this[NS] = 'root';
        options[NS] = 'root';
        nameSpace.set('root', this);
    }
    
    // 判断是否设置了 modules
    if (Object.keys(modules).length > 0) {forEachObj(modules, function(key, subOption) {
        // 创立新的 store, 并将父级模块标识传递给子模块不便递归标识
        let instance = new Store(Object.assign(subOption, { [NS]: `${options[NS]}/${key}` })
        );
        // 标记以后模块
        instance[NS] = `${options[NS]}/${key}`;
        
        nameSpace.set(instance[NS], instance);
        
        // 将以后模块的 state 注入到 父级 state 中 
        state[key] = instance.state;
      });
    }
    
    // 省略局部代码
  }

    // 省略局部代码
}

// 省略局部代码装载实例

尽管没有 Vuex 那样欠缺和弱小的模块零碎,不过咱们的重点是 mini 就好。(次要是我太菜了(╥╯^╰╥))

看下咱们的残缺例子吧~

定义 Modules

Store 对象

残缺代码亲戳这里

辅助函数

MiniVuex 提前封装了 mapStatemapGettersmapActionsmapMutations
对辅助办法的应用不太熟悉的戳这里。

辅助办法能够从 store 中取出对应的属性,同时也反对 模块查找 取值筛选 对取值进行更改名称

取值筛选 对取值进行更改名称 咱们能够在 utils.js 中定义函数来解决。

utils.js

因为属性筛选如果是 string[], 只须要简略的取值并且返回进来即可,而如果 object[] 的话就不单单取值,如果是对象的话能够对取出来的属性进行更改属性名更改,例如{count: 'newCount'}, 就是将取出来的属性能够以 nnewCount 进行拜访,防止过多属性带来的重名抵触。

所以咱们能够封装一个函数对这几种状况进行特点的解决。

// 封装对象或者数组
export const normalizeMap = map => {return Array.isArray(map)
    ? map.map(key => ({ key, val: key}))
    : Object.keys(map).map(key => ({ key, val: map[key] }));
};

如果传递的是 ['count'] 就返回 [{key: 'count',val: 'count'}];

如果是{count: state => state.count, user: "userInfo"} 就返回

[
    {
        key: 'count',
        val: state => state.count
    },
    {
        key: 'user',
        val: 'userInfo'
    }
]

而后在封装辅助办法的时候咱们还须要借助一下的几个工具办法来帮助开发。

// 判断是否是可遍历的对象
export const isValidMap = function isValidMap(map) {return Array.isArray(map) || isObject(map);
};
判断是否是对象
export const isObject = function isObject(obj) {return obj !== null && typeof obj === 'object';};

mapState

MiniVuex/helper.js

import {normalizeMap, isValidMap} from './utils';

const mapState = function(...arg) {
  let namespace = 'root',
    filters = [],
    res = {};

  if (arg.length === 1 && isValidMap(arg[0])) {filters = arg[0];
  } else if (
    arg.length >= 2 &&
    typeof arg[0] === 'string' &&
    isValidMap(arg[1])
  ) {namespace = `${namespace}/${arg[0]}`;
    filters = arg[1];
  } else {console.warn('[Vuex]: 参数异样哦哦哦~');
    return res;
  }

  // 解决筛选数据,并把对应模块(nameSpace)的 store 取出对应的状态 这一操作封装成一个(mappedState)办法,以 {[key]: mappedState} 的对象打包返回进来
  normalizeMap(filters).forEach(({key, val}) => {res[key] = function mappedState() {
      return typeof val === 'function'
        ? val.call(this, nameSpace.get(namespace).state)
        : nameSpace.get(namespace).state[val];
    };
  });

  return res;
};

另外的三个辅助办法都是一样的做法,就不多说了。

mapGetters

const mapGetters = function(...arg) {
  let namespace = 'root',
    filters = [],
    res = {};

  if (arg.length === 1 && isValidMap(arg[0])) {filters = arg[0];
  } else if (
    arg.length >= 2 &&
    typeof arg[0] === 'string' &&
    isValidMap(arg[1])
  ) {namespace = `${namespace}/${arg[0]}`;
    filters = arg[1];
  } else {console.warn('[Vuex]: 参数异样哦哦哦~');
    return res;
  }

  normalizeMap(filters).forEach(({key, val}) => {res[key] = function mappedGetter() {
      return typeof val === 'function'
        ? val.call(this, nameSpace.get(namespace).getters)
        : nameSpace.get(namespace).getters[val];
    };
  });

  return res;
};

mapMutations

const mapMutations = function(...arg) {
  let namespace = 'root',
    filters = [],
    res = {};

  if (arg.length === 1 && isValidMap(arg[0])) {filters = arg[0];
  } else if (
    arg.length >= 2 &&
    typeof arg[0] === 'string' &&
    isValidMap(arg[1])
  ) {namespace = `${namespace}/${arg[0]}`;
    filters = arg[1];
  } else {console.warn('[Vuex]: 参数异样哦哦哦~');
    return res;
  }

  normalizeMap(filters).forEach(({key, val}) => {res[key] = function mappedMutation(...args) {console.log(...args, nameSpace.get(namespace).commit(val, ...args));
    };
  });

  return res;
};

mapActions

const mapActions = function(...arg) {
  let namespace = 'root',
    filters = [],
    res = {};

  if (arg.length === 1 && isValidMap(arg[0])) {filters = arg[0];
  } else if (
    arg.length >= 2 &&
    typeof arg[0] === 'string' &&
    isValidMap(arg[1])
  ) {namespace = `${namespace}/${arg[0]}`;
    filters = arg[1];
  } else {console.warn('[Vuex]: 参数异样哦哦哦~');
    return res;
  }

  normalizeMap(filters).forEach(({key, val}) => {res[key] = function mappedMutation(...args) {console.log(...args, nameSpace.get(namespace).dispatch(val, ...args));
    };
  });

  return res;
};

以上就是 MiniVuex 的全副实现了。

MiniVuex 特点:

  1. 反对创立 Store
  2. 反对 modules 模块化贮存
  3. 反对 mapState, mapGetters, mapMutations, mapActions,应用办法同官网 Vuex 一样,反对模块化。

具体请查看源代码,以上传到 Github。

原创不易,欢送小伙伴记得关注微信公众号『进击的切图仔』Get 更多原创好文哦。

正文完
 0