简易版myVueX

249 阅读5分钟

VueX

在之前写的文章中,已经对Vue组件通信的N种方式进行了总结,VueX 作为Vue组件通信的最终解决方案,今天就来总结一下VueX。

Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式。它采用集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化。Vuex 也集成到 Vue 的官方调试工具 devtools extension ,提供了诸如零配置的 time-travel 调试、状态快照导入导出等高级调试功能。

引用官方文档中对VueX的介绍,其主要设计思想主要为集中式可预测,同时他也是与Vue有着强耦合的。

引用官方文档中的一张数据流向图

从图上可以看出,VueX的数据流向依旧是遵从单向数据流原则的。

在图中,有3个核心的概念:

State:作为所有数据的存放位置。

Mutations:修改数据的 唯一 方式。

Actions:扩展mutation存在的不足(必须为同步事件),在action中可以使用异步的事件,最终在异步事件完成后调用mutation去修改数据

VueX的使用

接下来简单介绍VueX的使用,更多详细的使用方式可以去官网中学习并自己跑一下demo。

引入

VueX作为Vue的一个插件库,其使用方法就显而易见了。

  1. 创建一个store.js,并引入需要的库
  2. 作为一个插件,肯定需要Vue.ues()一下
  3. new一个VueX.Store实例
  4. 导出实例
  5. 在main.js中注册一下
// 1.先创建一个store.js
// 引入Vue和VueX库
import Vue from 'vue'
import Vuex from 'vuex'

// 2.安装VueX插件
Vue.use(Vuex)

// 3.定义配置项,注意!!! new的是VueX.Store实例
// 4. 导出实例
export default new Vuex.Store({
  state: {
  },
  mutations: {
  },
  actions: {
  },
  modules: {
  }
})

import Vue from 'vue'
import App from './App.vue'
// 在main.js中引入创建的VueX的store
import store from './store'

Vue.config.productionTip = false

new Vue({
  // 5. 在vue实例中注册
  store,
  render: h => h(App)
}).$mount('#app')

简单使用介绍

简单写个例子,四个api直接使用

// 在 Vuex.Store 中定义各个项
export default new Vuex.Store({
  state: {
    // 直接定义值
    count: 0
  },
  
  getters:{
    // getters 可以当作计算属性来理解,最后需要返回计算完的值
    // getters,接受两个形参,注意是形参,第一个为state,第二个为getters 
    doubleCount(state,getters){
      return state.count * getters.diploid
    },
    diploid(){
      return 2
    }
    // 最后 getters 可以返回一个函数,相当于以方法的调用,这样就可以进行传参
    // doubleCount: (state) => (diploid) => {
    //   return state.count * diploid
    // },
  },
  
  mutations: {
    // mutations中需要设置事件类型,回调函数,然后通过commit进行触发
    // 回调函数会默认接受 state作为第一个形参,后续为commit是传入的参数
    // !!注意1: mutations中的回调函数必须为同步事件
    // !!注意2: VueX利用的是vue底层的响应式原理,所以在修改值时,比如对obj添加新的属性
    // 同样需要通过Vue.set去添加
    add(state){
      ++state.count
    },
    addAny(state, num){
      state.count += num
    }
  },
  
  
  actions: {
    // actions 会传入一个上下文(同样为形参),用来commit=》mutations事件
    // 上下文中可以获取到 commit、state、getters 等
    // !!注意:由于VueX.Store有模块的存在,所以此处传入的时一个上下文
    // actions 需要用dispatch来触发
    addPuls(context){
      setTimeout(() => {
        context.commit('addAny',2)
      }, 1000);
    }
    // 也可以用解构的方式获取commit
    // addPuls({ commit }){
    //   setTimeout(() => {
    //     commit('addAny',2)
    //   }, 1000);
    }
  },
})
// app组件
<template>
  <div id="app">
    <p>当前个数:{{ count }}</p>
    <button @click="add">+1</button><br>
    <button @click="addPuls">蓄力+</button><br>
    <input type="text" v-model.number="val"><button @click="addAny">任意+</button>
    <hello-world></hello-world>
  </div>
</template>

<script>
import HelloWorld from './components/HelloWorld'
export default {
  name: 'App',
  components:{
    HelloWorld
  },
  methods:{
    add(){
    // 使用commit,提交需要触发的mutations事件
      this.$store.commit('add')
    },
    addPuls(){
    // 使用dispatch,提交需要触发的actions事件
      this.$store.dispatch('addPuls')
    },
    addAny(){
    // 提交commit的时,可以传参
      this.$store.commit('addAny',this.val)
    },
  },
  // Vuex 的状态存储是响应式的,从 store 实例中读取状态最简单的方法就是在计算属性 中返回某个状态
  computed: {
    count () {
      return this.$store.state.count
    }
  },
  data() {
    return {
      val: 0
    }
  },
}
</script>
// HelloWorld子组件
<template>
  <div class="hello">
    <h1> 子组件中的值: {{ this.$store.state.count }}</h1>
    <h1> 子组件中的双倍值: {{ this.$store.getters.doubleCount }}</h1>
  </div>
</template>

<script>
export default {
  name: 'HelloWorld',
}
</script>

运行使用,commit和dispatch均顺利触发

稍微进阶使用

每次使用VueX,无论是引用值或者修改值,都需要this.$store.state.XXX或this.$store.commit('XXX'),是比较麻烦的,VueX中提供了相应的映射方法,用于简便使用。

<template>
  <div id="app">
    <p>当前个数:{{ count }}</p>
    <button @click="add">+1</button><br>
    <button @click="addPuls">蓄力+</button><br>
    <input
      type="text"
      v-model.number="val"
    ><button @click="addAny">任意+</button>
    <hello-world></hello-world>
    <p>count值:{{ count }}</p>
    <p>countAlias值:{{ countAlias }}</p>
    <p>countPlusLocalState值:{{ countPlusLocalState }}</p>
  </div>
</template>

<script>
import HelloWorld from './components/HelloWorld';
import { mapState } from 'vuex';

export default {
  // 映射VueX中的state
  name: 'App',
  components: {
    HelloWorld
  },
  methods: {
    add() {
      this.$store.commit('add');
    },
    addPuls() {
      this.$store.dispatch('addPuls');
    },
    addAny() {
      this.$store.commit('addAny', this.val);
    }
  },
  // 同时,打印一下看mapState
  mounted() {
    console.log(
      'mapState',
      mapState({
        count: state => state.count,
        countAlias: 'count',
        countPlusLocalState(state) {
          return state.count + this.localCount;
        }
      })
    );
  },
  computed: {
    ...mapState({
      count: state => state.count,

      // 传字符串参数 'count' 等同于 `state => state.count`
      countAlias: 'count',

      // 为了能够使用 `this` 获取局部状态,必须使用常规函数
      countPlusLocalState(state) {
        return state.count + this.localCount;
      }
    })
  },
  data() {
    return {
      val: 0,
      localCount: 10
    };
  }
};
</script>

可以看到,mapstate最后是一个对象,每个属性都是一个方法,

而在computer中,是可以接受一个函数的,所以就可以通过解构的方法将state映射到computer上。

同理可以得到其他映射方法的使用,当然,更具体的可以去官网看。

mpaGetters

  computed: {
    ...mapGetters({
      count: 'doubleCount'
    })
    // 可以直接为一个数组 ['doubleCount'],当然这样就需要命名一致了
  },

mapMutations

   methods: {
    ...mapMutations({
      add: 'add'
    })
    // 可以为数组 ['add']
  }

mapActions

   methods: {
    ...mapActions({
      addPuls: 'addPuls'
    })
    // 同样可以为数组 ['addPuls']
  }

模块化与插件

最后随着应用规模的逐渐壮大,内部逻辑变得十分复杂,就需要我们对vuex进行模块划分

引用官方的例子作为样例


const moduleA = {
  state: () => ({ ... }),
  mutations: { ... },
  actions: { ... },
  getters: { ... }
}

const moduleB = {
  state: () => ({ ... }),
  mutations: { ... },
  actions: { ... }
}

const store = new Vuex.Store({
  modules: {
    a: moduleA,
    b: moduleB
  }
})

store.state.a // -> moduleA 的状态
store.state.b // -> moduleB 的状态

最后,Vuex 还可以通过插件进行拓展,比如对VueX的数据进行持久化等,vuex 的 store 提供 plugins 的配置项,通过这个配置项可以安装插件。Vuex 插件就是一个函数,它接收 store 作为唯一参数:

const myPlugin = store => {
  // 当 store 初始化后调用
  store.subscribe((mutation, state) => {
    // 每次 mutation 之后调用
    // mutation 的格式为 { type, payload }
    console.log('====================================');
    console.log(mutation, state);
    console.log('====================================');
  })
}

然后像这样使用:

const store = new Vuex.Store({
  // ...
  plugins: [myPlugin]
})

在界面执行

实现 MyVueX

根据以上的使用介绍,来自己造一个简易版的VueX。

install实现

// myVueX.js
let Vue

// 定义store类
class Store {
  constructor(options = {}) {
    this.state = options.state
  }
}

// 创建install方法
function install(_vue) {
  Vue = _vue
  Vue.mixin({
  // 因需要等vue实例创建时,获取到store,再将其挂载到prototype上,
  // 所以需要将install函数执行的时间延后,常规套路就是使用mixin在beforeCreate的时候执行
    beforeCreate() {
    // 只有在根实例的时候才添加store
      if (this.$options.store) {
        Vue.prototype.$store = this.$options.store
      }
    }
  })
}

// 导出install和Store
export default {
  install,
  Store
}
// store.js
import Vue from 'vue'
import Vuex from './myVueX'

Vue.use(Vuex)
// 先简单定义个state
export default new Vuex.Store({
  state: {
    name: 'myVueX'
  }
})
// app组件
<template>
  <div id="app">
  // 在页面中展示
    <p>App组件{{ this.$store.state.name }}</p>
  </div>
</template>

<script>
export default {
  name: 'App'
};
</script>

可以看到 页面中的state已经成功获取到值

mutations

现在实现mutations配置和commit方法

let Vue

class Store {
  constructor(options = {}) {
    this.state = options.state
    this._mutations = options.mutations
  }
  commit(type, ...args) {
    console.log(type, args);
    const entry = this._mutations[type]
    if (!entry ) {
      console.error(`unknown mutation type: ${type}`);
      return
    }
    entry(this.state, ...args)
  }
}

function install(_vue) {
  Vue = _vue
  Vue.mixin({
    beforeCreate() {
      if (this.$options.store) {
        Vue.prototype.$store = this.$options.store
      }
    }
  })
}

export default {
  install,
  Store
}
export default new Vuex.Store({
  state: {
    name: 'myVueX',
    count: 0
  },
  mutations: {
    add(state) {
      ++state.count
      console.log(state);
    },
  },
})
<template>
  <div id="app">
    <p>App组件{{ this.$store.state.name }}</p>
    <p>数值:{{ this.$store.state.count }}</p>
    <button @click="add">+1</button><br>
  </div>
</template>

<script>
export default {
  name: 'App',
  methods: {
    add() {
      this.$store.commit('add');
    }
  }
};
</script>

在页面中运行,可以看到commit触发的事件执行了,但是数据并没有刷新

state响应式

现在需要解决数据响应式的问题,对于数据响应式,完全可以利用vue本身提供的特性来实现。

class Store {
  ...
  constructor(options = {}) {
    // 利用vue的数据响应式特性,来创建一个响应式的数据
    this._vm = (new Vue({
      data: {
        ?state: options.state
      }
    }));

    this.state = this._vm._data.?state
    this._mutations = options.mutations
  }
  ...
}

现在再来看一下,数据已经可以响应式刷新了

稍微优化一下,让state不可以被覆盖

  constructor(options = {}) {
    // 利用vue的数据响应式特性,来创建一个响应式的数据
    this._vm = (new Vue({
      data: {
        ?state: options.state
      }
    }));

    this._mutations = options.mutations
  }
  // get state时将做好响应式处理的数据返回
  get state() {
    return this._vm._data.?state_data
  }
  // 当想直接覆盖state时,提示不允许被覆盖
  set state(val) {
    console.error('state 不允许被覆盖');
  }

action

class Store {
  constructor(options = {}) {
    // 利用vue的数据响应式特性,来创建一个响应式的数据
    this.vm = (new Vue({
      data: {
        ?state: options.state
      }
    }).);

    this._mutations = options.mutations
    this._actions = options.actions
  }
  ...
    dispatch(type, ...args) {
    const entry = this._actions[type]
    if (!entry) {
      console.error(`unknown actions type: ${type}`);
      return
    }
    entry(this, ...args)
  }
  ...
actions: {
    addPuls(context) {
      setTimeout(() => {
        context.commit('add')
      }, 1000);
    }
  },

在页面中运行,可以顺利执行

getter

class Store {
  constructor(options = {}) {
  ...
    const getters = {}
    for (let x in options.getters) {
      Object.defineProperty(getters, x, {
        get: () => {
          return options.getters[x](this.state)
        }
      })
    }
    this.getters = getters
  ...
  }
  getters: {
    doubleCount(state) {
      return state.count * 2
    },
  },

在页面中运行

最后挖坑

以上基本是可以实现一个最简单版的 VueX 了

但是模块化,映射的辅助函数,直接修改state的问题,均没有实现,先挖好坑,后续填上,以及自己实现一个 VueX 插件,加深对 VueX 的使用以及理解。