【Vue.js】使用 Vue 的插件化实现 Vuex 的基本功能

提前说明:

  1. 本文不考虑使用 module 的情况,只考虑 vuex 的基本功能
  2. 本文不考虑多个 store 的情况
  3. 项目路径可能比较简单,如果需要扩展请读者自行调整一下

回顾一下 Vuex 如何使用?

  1. 声明一个 store
js 复制代码
import { createStore } from 'vuex';

const store = createStore({
  state: {
    count: 0
  },
  mutations: {
    increment(state) {
      state.count++;
    },
    decrement(state) {
      state.count--;
    },
    reset(state, payload) {
      state.count = payload || 0;
    }
  },
  actions: {
    reset({ commit }, payload) {
      console.log(commit, payload);
      if (typeof payload === 'number') {
        commit('reset', payload);
      }
    }
  },
  getters: {
    doubleCount(state, getters) {
      return state.count * 2;
    }
  },
});

export default store;
  1. 在 app 中使用插件:
js 复制代码
import { createApp } from 'vue';
import App from './App.vue';
import './index.css';

import store from './store';

createApp(App).use(store).mount('#app');
  1. App.vue中使用:
html 复制代码
<script setup>
  import { computed } from 'vue';
  import { useStore } from 'my-vuex';

  const store = useStore();

  const count = computed(() => store.state.count);

  const doubleCount = computed(() => store.getters.doubleCount);

  const addCount = () => {
    store.commit('increment');
  }

  const resetCount = () => {
    store.dispatch('reset', 3);
  }

</script>

<template>
  <div>
    <h1>Count: {{ count }}</h1>
    <h1>DoubleCount: {{ doubleCount }}</h1>

    <button @click="addCount">Add Count</button>
    <button @click="$store.commit('decrement')">Minus Count</button>
    <button @click="resetCount">Reset Count</button>
  </div>
</template>

思考:

假如我想要实现 vuex 的基本功能,命名为 my-vuex 该怎么办呢?

实现思路:

  1. my-vuex的整体目录结构初定:
perl 复制代码
my-vuex
|- index.js     # 项目的出口
|- creator.js   # 创建工具的方法
|- hooks.js     # 对外抛出的一些 hooks
  1. 程序从外往里设计 - my-vuex 中导出两个方法:createStoreuseStore
js 复制代码
/* 构建器 */
export { createStore } from './creator';

/* 仓库需要用到的 hooks */
export { useStore } from './hooks';
  1. my-vuex 根目录下面创建一个配置文件 config.js,这里我们需要一个 唯一的 store 名称
js 复制代码
/** store 的名称 */
export const STORE_NAME = Symbol('store');
  1. 实现一下 creator/createStore方法,要求如下:

    1. 这个方法接收 store 的配置选项,并需要封装代理这些 store 配置功能(这里我们就是用面向对象的设计方式进行处理)
    2. 这个方法返回一个 Vue 的插件, 返回的插件需要完成两件事:
      1. 向下注入 store
      2. 挂载全局对象 $store
js 复制代码
import { STORE_NAME } from './config';
import Store from './Store';

/**
 * @function createStore
 * @description 创建一个 store
 * @param {object} config 配置 store 的选项
 * @param {Record<string, any>} config.state store 需要的状态
 * @param {{ [key: string]: ((state: Record<string, any>, payload: any) => void) }} config.mutations 变更 store 状态的方法集合
 * @param {{ [key: string]: ((store: Store, payload: any) => void) }} config.actions 定义变更 store 值的 action
 * @param {{ [key: string]: ((state: Record<string, any>, getters: any) => any) }} config.getters 定义计算获取的数据
 * @return {{ install: (app: import('vue').App, options: any) => void }} 返回一个 store 插件
 */
export function createStore(config) {
  try {
    const { state, mutations, actions, getters } = config;
    const store = new Store({
      state,
      mutations,
      actions,
      getters
    });

    return {
      install(app, options) {
        app.provide(STORE_NAME, store);
        app.config.globalProperties.$store = store;
      }
    };
  } catch (e) {
    throw new Error(e);
  }
}
  1. 实现一下 hooks/useStore方法,这里直接使用 inject(STORE_NAME)即可:
js 复制代码
import { inject } from 'vue';

import { STORE_NAME } from './config';

/**
 * 在组件中使用 store
 */
export function useStore() {
  return inject(STORE_NAME);
}
  1. 实现一下 Store 这个类,要求如下:

    1. 需要有一个公有的可读属性:state
    2. 需要有一个公有的可读属性:getters
    3. 需要有一个 dispatch(type, payload) 方法
    4. 需要有一个 commit(type, payload) 方法
    5. 内部需要有一个初始化代理的 _init() 方法
js 复制代码
import { reactive } from 'vue';

class Store {
  constructor({
    state,
    mutations,
    actions,
    getters,
  }) {
    // 把原来的配置存储在实例中
    this._state = reactive(state);
    this._mutations = mutations;
    this._actions = actions;
    this._getters = getters;

    this._init();
  }

  _init() {
    // ... 初始化操作
  }

  dispatch(type, payload) {}

  commit(type, payload) {}
}

export default Store;
  1. 设置只读属性 state
js 复制代码
import { reactive } from 'vue';

class Store {
	actions = {};

  mutations = {};

  getters = {};
  
  constructor({
    state,
    mutations,
    actions,
    getters,
  }) {
    // 把原来的配置存储在实例中
    this._state = reactive(state);
    this._mutations = mutations;
    this._actions = actions;
    this._getters = getters;

    this._init();
  }

  // 在这里加入 state getter
  get state() {
    return this._state;
  }

  _init() {
    // ... 初始化操作
  }

  dispatch(type, payload) {}

  commit(type, payload) {}
}

export default Store;
  1. 实现一下 init 方法,要求如下:

    • 代理 mutations 中的方法
    • 代理 actions 中的方法
    • 代理 getters 中的内容

所以,我们在这里需要在 _init()方法中调用三个方法:

  • initMutations
  • initActions
  • initGetters
js 复制代码
import { reactive } from 'vue';
import {
  initActions,
  initMutations,
  initGetters
} from './init';

class Store {
	actions = {};

  mutations = {};

  getters = {};
  
  constructor({
    state,
    mutations,
    actions,
    getters,
  }) {
    // 把原来的配置存储在实例中
    this._state = reactive(state);
    this._mutations = mutations;
    this._actions = actions;
    this._getters = getters;

    this._init();
  }

  get state() {
    return this._state;
  }

  _init() {
    // ... 初始化操作
    initMutations(this._mutations, this);
    initActions(this._actions, this);
    initGetters(this._getters, this);
  }

  dispatch(type, payload) {}

  commit(type, payload) {}
}

export default Store;
  1. my-vuex中创建一个 init.js文件,并在文件中分别实现一下initMutationsinitActionsinitGetters 这三个方法:
  • initMutations
js 复制代码
/**
 * @function initMutations
 * @description 初始化 mutations 中的方法
 * @param {{ [key: string]: ((state: Record<string, any>, payload: any) => void) }} mutations 变更 store 状态的方法集合
 * @param {import('./Store').default} store
 */
export function initMutations(mutations, store) {
  store.mutations = {};
  for (let key in mutations) {
    if (mutations.hasOwnProperty(key)) {
      const fn = mutations[key].bind(store);
      Object.defineProperty(store.mutations, key, {
        get() {
          return (payload) => {
            fn(store._state, payload);
          };
        }
      });
    }
  }
}
  • initActions
js 复制代码
/**
 * @function initActions
 * @description 初始化 actions 中的方法
 * @param {{ [key: string]: ((store: Store, payload: any) => void) }} actions 定义变更 store 值的 action
 * @param {import('./Store').default} store
 */
export function initActions(actions, store) {
  store.actions = {};
  for (let key in actions) {
    if (actions.hasOwnProperty(key)) {
      const commit = store.commit.bind(store);
      Object.defineProperty(store.actions, key, {
        get() {
          return (payload) => {
            console.log(key, payload)
            commit(key, payload);
          }
        }
      });
    }
  }
}
  • initGetters
js 复制代码
/**
 * @function initGetters
 * @description 初始化 actions 中的 getters
 * @param {} getters
 */
export function initGetters(getters, store) {
  store.getters = {};

  for (let key in getters) {
    if (getters.hasOwnProperty(key)) {
      const fn = store._getters[key].bind(store);
      Object.defineProperty(store.getters, key, {
        get() {
          return computed(() => {
            return fn(store._state, store.getters);
          });
        }
      });
    }
  }
}

到这里,我们的代码功能就已经实现完成了 😄😄😄

完整代码, 参考:

📎vuex-impl.zip

相关推荐
杨荧4 分钟前
【JAVA毕业设计】基于Vue和SpringBoot的宠物咖啡馆平台
java·开发语言·jvm·vue.js·spring boot·spring cloud·开源
zhanghaisong_201514 分钟前
Caused by: org.attoparser.ParseException:
前端·javascript·html·thymeleaf
南城夏季1 小时前
蓝领招聘二期笔记
前端·javascript·笔记
Huazie1 小时前
来花个几分钟,轻松掌握 Hexo Diversity 主题配置内容
前端·javascript·hexo
NoloveisGod1 小时前
Vue的基础使用
前端·javascript·vue.js
GISer_Jing1 小时前
前端系统设计面试题(二)Javascript\Vue
前端·javascript·vue.js
海上彼尚2 小时前
实现3D热力图
前端·javascript·3d
理想不理想v2 小时前
使用JS实现文件流转换excel?
java·前端·javascript·css·vue.js·spring·面试
EasyNTS2 小时前
无插件H5播放器EasyPlayer.js网页web无插件播放器vue和react详细介绍
前端·javascript·vue.js
老码沉思录3 小时前
React Native 全栈开发实战班 - 数据管理与状态之Zustand应用
javascript·react native·react.js