用100行代码实现一个 toy vuex

324 阅读1分钟

前言

最近看了一下 vuex 的实现,使用的是 watch 来对数据源进行监听。于是我在想可不可以用更简单的方式来实现对数据的双向绑定。

得益于之前写过的# Vue3 TypeScript 全局 Message 提示框。想着可不可以使用 vue component 中 data 天然的数据双向绑定来实现呢?

话不多说,让我们来验证一下。

总流程

流程图.png

具体代码实现

文件目录

image.png

index.js

// index.ts
import { render, createVNode } from 'vue';
import Store from './index.vue';

let storeConfig = {};
let store;
const createStore = (opts) => {
  const { modules } = opts;
  storeConfig = modules;
  // 创建一个 div 容器
  const container = document.createElement('div');
  // 创建 Store 实例,createVNode 的性能比 h 更好
  const vm = createVNode(Store, { storeProp: storeConfig });
  // 把实例 render 到容器上
  render(vm, container);
  store = { data: vm.component.data.store };
  initConfig(store);
  return {
    install: (app) => {
      app.config.globalProperties.$simpleStore = store;
    },
  };
};
const getModule = moduleName => store?.data?.[moduleName];
const toyMapState = (moduleName, paramArr) => {
  getModule();
  const res = {};
  paramArr.forEach(async (param) => {
    res[param] = () => {
      if (store) {
        const module = getModule(moduleName);
        return module?.state?.[param];
      }
    };
  });
  return res;
};
const initConfig = (store) => {
  store.commit = (moduleName, funName, data) => {
    const module = getModule(moduleName);
    module.mutations[funName](module.state, data);
  };
  // dispatch 同理
};
export { toyMapState, createStore };

index.vue

<template>
  <div></div>
</template>

<script>
export default {
  props: {
    storeProp: {
      type: Object,
      default: () => ({}),
    },
  },
  data() {
    return {
      store: {},
    };
  },
  created() {
    this.store = this.storeProp;
  },
};
</script>

store.js

import { createStore } from './index';
export default createStore({
  modules: {
    moduleA: {
      state: { a: 1234 },
      mutations: {
        addA(state, num) {
          state.a += num;
        },
      },
    },
    moduleB: {
      state: { b: 2468 },
      mutations: {
        reduceB(state, num) {
          state.b -= num;
        },
      },
    },
  },
});

mian.js

// 在 main.js 增加代码
import SimpleStore from './store/install/store';

app.use(SimpleStore);

demo 示例

// demo.vue
<template>
  <div>
    {{ showData }}
    <div>{{ a }} -------------</div>
    <div @click="handleClickA">moduleA a +1</div>
    <div @click="commitA">moduleA a +10 commit</div>
    <div>{{ b }} -------------</div>
    <div @click="handleClickB">moduleB b -1</div>
    <div @click="commitB">moduleB a -10 commit</div>
    <router-view />
  </div>
</template>

<script>
import { toyMapState } from './store/install';

export default {
  components: {},
  watch: {
    a(newValue, oldValue) {
      console.log(newValue, oldValue, 'a change');
    },
  },
  computed: {
    ...toyMapState('moduleA', ['a']),
    ...toyMapState('moduleB', ['b']),
    showData() {
      return JSON.stringify(this.$simpleStore);
    },
  },
  created() {
    console.log(this.$simpleStore, this.a, 'simpleStore');
  },
  methods: {
    handleClickA() {
      const moduleA = this.$simpleStore.data.moduleA.state;
      moduleA.a += 1;
    },
    handleClickB() {
      const moduleB = this.$simpleStore.data.moduleB.state;
      moduleB.b -= 1;
    },
    commitA() {
      this.$simpleStore.commit('moduleA', 'addA', 10);
    },
    commitB() {
      this.$simpleStore.commit('moduleB', 'reduceB', 10);
    },
  },
};
</script>

总结

通过上述的方式就可以简单的构建一个自己的 store 状态库了。

遇到的问题

  1. computed 中 toyMapState 的函数是在 main.js 之前执行的。此时 store 还没有初始化。

当时一直想着怎么样让 toyMapState 中的对象延迟到 store 初始化后返回,但是实现不了。后来发现只需要判断一下 store 初始化数据没有再来返回值即可。本质上返回的是一个函数,生命周期函数是在 main.js 执行后才执行。