实现一个自己的vuex

405 阅读4分钟

概述

距离vue3x出来已经一年多了,现在差不多新项目可以使用3.0以上版本的了,很多对应生态和ui库都进行了升级,总之对应vue3x还是带来了很多的便利的,最好的特性当属组合式API,我们可以更加方便的抽离我们的模块代码了。对于vue状态管理,vue官方除了将原来vuex升级为4x版本,还推出了pinia,总之本质上都是状态管理,用那个取决于自己,只不过vue3更加推荐使用pinia而已,出于对vuex状态管理的实现原理的好奇,平时开发也自己经常用vuex,还是很好奇怎么实现的,现自己实现一个vuex,看完相信你自己也能实现。

为什么要使用状态管理

对于我们开发项目,可能最需要共享的状态数据就是登录用户信息,我们肯能跨不同页面都要用到,不能只放到一个组件当中,因此需要全局共享,这是我们使用状态管理的目的。

vuex的实现流程图

image.png 总结出我们需要完成的几个点:

  • 状态存在state当中,state中的数据是响应式的
  • 我们更改状态dispatch===>commit====>state,在没有异步操作的时候,可以直接commit===>state,这是vuex工作的整个流程

实现本质

综上:我们直白点来说其实就是想办法将state中的数据响应式化,然后通过各种中规中矩的操作去改这个state中的状态,当响应式数据变化的时候,视图更新。这就是本质

最终效果

动画.gif

实现目标

我们这里实现vue中的:

  • state
  • mutations
  • actions
  • getters
  • modules(自行扩展)

目录结构

image.png

myStore/index.js

正常定义仓库的文件,正常配置和vuex保持一致

import {createStore} from "./myStore";
// 创建一个新的 store 实例
const myStore = createStore({
  state() {
    return {
      count: 0,
      list: [1],
    };
  },
  mutations: {
    increment(state) {
      state.count++;
    },
    addListItem(state, payload) {
      state.list.push(payload);
    },
  },
  actions: {
    addListItem({ commit }, payload) {
      commit("addListItem", payload);
    },
  },
  getters: {
    computedCount(state) {
      return state.count + "--";
    },
    listToString(state) {
      return state.list.join("");
    },
  },
});


export default myStore;

myStore/index.js

这个文件就是我们的核心实现文件

import { reactive, computed, inject } from "vue";
// store类
class MyStore {
  constructor(options) {
    // 结构createStore传入的options
    const { state, mutations, actions, getters } = options;
    // 代理state,做一层包裹(状态管理的核心,通过响应式包裹的对象会在模板用到的时候进行依赖收集)从而数据变化实现响应式更新
    this._state = reactive({ data: state() });
    // 将用户传入的配置项复刻到当前实例上面
    this.createMutations(mutations);
    this.createActions(actions);
    this.createGeters(getters);
    // 记得将commit函数的this强制绑定为store实例对象,不然action里面函数解构commit的时候,this会丢
    this.commit = this.commit.bind(this);
  }
  //   vue插件必用的函数,app.use的时候会调用这个函数
  install(app) {
    // 将当前实例放到globalProperties(相当于vue2版本挂载到prototype)目的是在模板上$store.state.count能访问到
    app.config.globalProperties.$myStore = this;
    // 通过provide将当前store注入到所有的子组件中,然后就可以通过useStore访问到
    app.provide("myStore", this);
  }
  //   做一层代理,访问state的时候,去拿_state中的data
  get state() {
    return this._state.data;
  }
  //   做一层代理,访问state的时候,去拿_getters中的计算属性
  get getters() {
    return this;
  }
  //   主要将外部调用createStore方法传入的options中的:
  //   mutations复刻到实例对象_mutations中,将原来mutations中的函数加强,通过函数包裹,传递额外参数和改变this指向为当前store实例
  createMutations(mutations) {
    this._munations = {};
    this.foreachOptionsToInstance(
      this._munations,
      mutations,
      (optionFn, payload) => {
        optionFn.bind(this)(this.state, payload);
      }
    );
  }
  //  同上
  createActions(actions) {
    this._actions = {};
    this.foreachOptionsToInstance(
      this._actions,
      actions,
      (optionFn, payload) => {
        optionFn.bind(this)(this, payload);
      }
    );
  }
  // getter函数我们使用computed包裹
  createGeters(getters) {
    this._getters = {};
    for (let key in getters) {
      this._getters[key] = computed(() =>
        getters[key].bind(this)(this.state, this._getters)
      );
      //   定义描述符,访问计算属性的时候,拿实例上_getters上的计算属性
      Object.defineProperty(this, key, {
        get() {
          return this._getters[key];
        },
      });
    }
  }
  /**
   * @Description commit方法,使用方法为:mystore.commit('increment',3)
   * @param { String } type commit的类型,为字符串要在mutations对象中找到
   * @param { any } paylod 外部调用传递的参数
   **/
  commit(type, paylod) {
    // 本质上就是调用mutations中的方法,然后传参
    this._munations[type](paylod);
  }
  /**
   * @Description dispatch方法,使用方法为:mystore.dispatch('increment',3)
   * @param { String } type dispatch的类型,为字符串要在actions对象中找到
   * @param { any } paylod 外部调用传递的参数
   **/
  dispatch(type, payload) {
    this._actions[type](payload);
  }

  //   用来遍历配置对象,将其复刻到实例上
  foreachOptionsToInstance(targetOption, originOption, callBack) {
    for (let key in originOption) {
      targetOption[key] = (payload) => {
        // 将这里执行的逻辑通过回调函数由外部处理
        // originOption[key].bind(bindThis)(bindThis, payload);
        callBack(originOption[key], payload);
      };
    }
  }
}

// 最终暴露给外部使用的创建store的函数
export function createStore(options) {
  return new MyStore(options);
}

// 在组件中使用的hook,调用就能拿到当sotre实例
export function useStore() {
    // 组件调用的时候,接手use后provide进来的store
  return inject("myStore");
}

注册组件

//main.js中
import { createApp } from "vue";
import App from "./App.vue";
import store from "./store/index.js";
import myStore from "./myStore/index.js";
createApp(App).use(store).use(myStore).mount("#app");

使用

<script setup>
//使用和vuex一样,只不过换成了我们自己的
import { useStore } from "./myStore/myStore";
const myStore = useStore();

// 测试commit
const addCount = () => {
  myStore.commit("increment");
};

// 测试dispatch
const addListIttem = () => {
  myStore.dispatch("addListItem", Math.random().toFixed(2));
};
</script>

<template>
  <div>
    <h2>this is MyVuex</h2>

    <p>
      <button @click="addCount">测试count响应式</button>
      <button @click="addListIttem">测试list响应式</button>
    </p>
    //可以像vue2版本一样直接在模板上面使用
    <p>count全局访问{{ $myStore.state.count }}</p>
    <p>count的值为{{ myStore.state.count }}</p>
    <p>count的计算属性:{{ myStore.computedCount }}</p>
    <ul>
      <li v-for="(item, index) in myStore.state.list" :key="index">
        {{ item }}
      </li>
    </ul>
    <p>list的计算属性:{{ $myStore.getters.listToString }}</p>
  </div>
</template>

总结

看完有没有觉得其实vuex的实现过程也不是很复杂,代码量也不多,其实我们自己完全可以写一个自己的全局状态管理工具,只是官方考虑的比我们的更加全面,各种容错处理更细致。上面代码量上也得益于vue3的组合式api。