VueX 基本使用和实现

190 阅读6分钟

一、简易的状态管理方案

  • 如果多个组件之间要共享状态(数据),单纯使用父子组件通讯、兄弟组件通讯的方式虽然可以实现,但是比较麻烦,而且多个组件之间互相传值很难跟踪数据的变化,如果出现问题很难定位问题

  • 当遇到多个组件需要共享状态的时候,典型的场景:购物车。如果单纯使用父子组件通讯、兄弟组件通讯的方案都不合适,并且会遇到以下的问题

    1. 多个视图依赖于同一状态
    2. 来自不同视图的行为需要变更同一状态
  • 对于问题一,传参的方法对于多层嵌套的组件将会非常繁琐,并且对于兄弟组件间的状态传递无能为力。对于问题二,我们经常会采用父子组件直接引用或者通过事件来变更和同步状态的多份拷贝。以上的这些模式非常脆弱,通常会导致无法维护的代码。

  • 因此,我们可以把组件的共享状态抽取出来,以一个全局单例模式管理,在这种模式下,我们的组件树构成了一个巨大的“视图”,不管在树的哪个位置,任何组件都能获取状态或者触发行为!我们可以把多个组件的状态,或者整个程序的状态放到一个集中的位置存储,并且可以检测到数据的更改。

现在不使用 Vuex ,先以一种简单的方式来实现

store.js

// 集中式状态管理(使用全局唯一的对象 store 来存储状态)
export default {
  debug: true, // 为了方便调试,如果为 true ,action 修改数据时候打印日志
  // state 用来储存状态
  state: {
    user: {
      name: 'xiaomao',
      age: 18,
      sex: '男'
    }
  },
  // action 用来修改状态 (组件不能直接修改 store 中的状态,必须调用 action 来更改)
  setUserNameAction(name) {
    if (this.debug) {
      console.log('setUserNameAction triggered:', name)
    }
    this.state.user.name = name
  }
}

componentA.vue

<template>
  <div>
    <h1>componentA</h1>
    user name: {{ sharedState.user.name }}
    <button @click="change">Change Info</button>
  </div>
</template>

<script>
import store from './store'
export default {
  methods: {
    change () {
      store.setUserNameAction('componentA')
    }
  },
  data () {
    return {
      privateState: {},
      sharedState: store.state
    }
  }
}
</script>

<style>

</style>

componentB.vue

<template>
  <div>
    <h1>componentB</h1>
    user name: {{ sharedState.user.name }}
    <button @click="change">Change Info</button>
  </div>
</template>

<script>
import store from './store'
export default {
  methods: {
    change () {
      store.setUserNameAction('componentB')
    }
  },
  data () {
    return {
      privateState: {},
      sharedState: store.state
    }
  }
}
</script>

<style>

</style>

二、VueX 基本概念

2-1 VueX 是什么?

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

  1. Vuex 是专门为 Vue.js 设计的状态管理库,就是一个 JavaScript 的第三方库
  2. Vuex 采用集中式的方式存储需要共享的数据
  3. Vuex 的作用是进行状态管理,解决复杂组件通信,数据共享

2-2 什么情况下使用 Vuex ?

官方文档:
Vuex 可以帮助我们管理共享状态,并附带了更多的概念和框架。这需要对短期和长期效益进行权衡。如果您不打算开发大型单页应用,使用 Vuex 可能是繁琐冗余的。确实是如此——如果您的应用够简单,您最好不要使用 Vuex。一个简单的 store 模式就足够您所需了。但是,如果您需要构建一个中大型单页应用,您很可能会考虑如何更好地在组件外部管理状态,Vuex 将会成为自然而然的选择。引用 Redux 的作者 Dan Abramov 的话说就是:Flux 架构就像眼镜:您自会知道什么时候需要它

总结:
单开发大型单页应用,并且多个视图依赖于同一状态,来自不同视图的行为需要变更同一状态时候,就符合使用 Vuex 来进行数据管理,典型场景就是购物车

三、VueX 核心概念和基本使用

image.png

3-1 VueX 基本结构

  1. 导入 VueX
import Vuex from 'vuex'
  1. 注册 VueX
Vue.use(Vuex)
  1. 注入 $store 到 Vue 实例
import store from './store'
new Vue({
  store,
  render: h => h(App)
}).$mount('#app')

3-2 State

  • Vuex 使用单一状态树,用一个对象 State 就包含了全部的应用层级状态
import Vue from 'vue'
import Vuex from 'vuex'

Vue.use(Vuex)

export default new Vuex.Store({
  strict: process.env.NODE_ENV !== 'production', // 开发模式中开启  严格模式,生产模式中开启严格模式会影响性能
  state: {
    count: 0,
    msg: 'Hello Vuex'
  },
  getters: {},
  mutations: {},
  actions: {},
  modules: {}
})

  • 组件可以通过 $store.state 来获取 state 中的状态
count:{{ $store.state.count }}
msg: {{ $store.state.msg }}
  • 同时也可以使用 mapState 简化 State 在视图中的使用,mapState 可以配合计算属性 computed 来使用,mapState 有两种使用的方式
  1. 接收数组参数
// 该方法是 vuex 提供的,所以使用前要先导入
import { mapState } from 'vuex'

// mapState(['count', 'msg']) 返回一个对象:
// { count: state => state.count, msg: state => state.msg}
computed: {
    ...mapState(['count', 'msg']),
}
  1. 接收对象参数(防止命名冲突)
// 该方法是 vuex 提供的,所以使用前要先导入
import { mapState } from 'vuex'

// mapState({ num: 'count', message: 'msg' }) 返回一个对象:
// { num: state => state.count, message: state => state.msg}
computed: {
    ...mapState({ num: 'count', message: 'msg' }),
}

3-3 Getter

  • Getter 就是 store 中的计算属性,主要还是用来对 state 中的数据做一些简单的处理
import Vue from 'vue'
import Vuex from 'vuex'

Vue.use(Vuex)

export default new Vuex.Store({
  state: {
    count: 0,
    msg: 'Hello Vuex'
  },
  getters: {
     reverseMsg (state) {
      return state.msg.split('').reverse().join('')
    }
  },
  mutations: {},
  actions: {},
  modules: {}
})

  • 组件可以通过 $store.getters 来获取 VueX 中的计算属性
reverseMsg: {{ $store.getters.reverseMsg }}
  • 同时使用 mapGetter 简化 getter 视图中的使用(用法和 mapState 一样)
import { mapGetter } from 'vuex'

computed: {
    ...mapGetter(['reverseMsg']),
    // 改名,在模板中使用 reverse
    ...mapGetter({
        reverse: 'reverseMsg'
    })
}

3-4 Mutation

  • 更改 Vuex 的 store 中的状态的唯一方法是提交 mutation。Vuex 中的 mutation 非常类似于事件:每个 mutation 都有一个事件名称和一个回调函数 (handler)。这个回调函数就是我们实际进行状态更改的地方,并且它会接受 state 作为第一个参数。

  • 使用 Mutation 改变状态的好处是,集中的一个位置对状态修改,不管在什么地方修改,都可以追踪到状态的修改。可以实现高级的 time-travel 调试功能

import Vue from 'vue'
import Vuex from 'vuex'

Vue.use(Vuex)

export default new Vuex.Store({
  state: {
    count: 0,
    msg: 'Hello Vuex'
  },
  getters: {
     reverseMsg (state) {
      return state.msg.split('').reverse().join('')
    }
  },
  mutations: {
    increate (state, payload) {
      state.count += payload
    }
  },
  actions: {},
  modules: {}
})
  • 组件可以直接调用 $store.commit() 来提交 mutation
 <button @click="$store.commit('increate', 2)">Mutation</button>
  • 同时也可以使用 mapMutations ,来简化视图对 mutation 的调用
Version:0.9 StartHTML:0000000105 EndHTML:0000002874 StartFragment:0000000141 EndFragment:0000002834

import { mapMutations } from 'vuex'

methods: {
    // mapMutations 其实就是返回一个对象:
    // { increate(payload): $store.commit('increate',payload)}
    ...mapMutations(['increate']),
    // 传对象解决重名的问题
    ...mapMutations({
        increateMut: 'increate'
    })
}

3-5 Action

  • Action 类似于 mutation,不同在于,Action 的目的是提交 mutation,而不是直接变更状态;Action 可以包含任意异步操作
import Vue from 'vue'
import Vuex from 'vuex'

Vue.use(Vuex)

export default new Vuex.Store({
  state: {
    count: 0,
    msg: 'Hello Vuex'
  },
  getters: {
     reverseMsg (state) {
      return state.msg.split('').reverse().join('')
    }
  },
  mutations: {
    increate (state, payload) {
      state.count += payload
    }
  },
  actions: {
      increateAsync (context, payload) {
          setTimeout(() => {
            context.commit('increate', payload)
          }, 2000)
        }
  },
  modules: {}
})
  • 组件可以直接调用 $store.dispatch 来调用 aciton
<button @click="$store.dispatch('increateAsync', 5)">Action</button>
  • 同时也可以使用 mapActions ,来简化视图对 aciton 的调用
import { mapActions } from 'vuex'

methods: {
    // mapActions 其实就是返回一个对象:
    // { increateAsync(payload): $store.dispatch('increateAsync',payload)}
    ...mapActions(['increate']),
    // 传对象解决重名的问题
    ...mapActions({
        increateAction2: 'increateAsync'
    })
}

3-6 Module

  • 由于使用单一状态树,应用的所有状态会集中到一个比较大的对象。当应用变得非常复杂时,store 对象就有可能变得相当臃肿。为了解决以上问题,Vuex 允许我们将 store 分割成模块(module)。每个模块拥有自己的 state、mutation、action、getter、甚至是嵌套子模块。

modules/products.js 模块

const state = {
  products: [
    { id: 1, title: 'iPhone 11', price: 8000 },
    { id: 2, title: 'iPhone 12', price: 10000 }
  ]
}
const getters = {}
const mutations = {
  setProducts(state, payload) {
    state.products = payload
  }
}
const actions = {}

export default {
  namespaced: true, // 开启命名空间
  state,
  getters,
  mutations,
  actions
}

modules/cart.js 模块

const state = {}
const getters = {}
const mutations = {}
const actions = {}

export default {
  namespaced: true, // 开启命名空间
  state,
  getters,
  mutations,
  actions
}

在 store 中导出

import Vue from 'vue'
import Vuex from 'vuex'
import products from './modules/products'
import cart from './modules/cart'

Vue.use(Vuex)

export default new Vuex.Store({
  strict: process.env.NODE_ENV !== 'production',
  state: {
    count: 0,
    msg: 'Hello Vuex'
  },
  getters: {
    reverseMsg(state) {
      return state.msg.split('').reverse().join('')
    }
  },
  mutations: {
    increate(state, payload) {
      state.count += payload
    }
  },
  actions: {
    increateAsync(context, payload) {
      setTimeout(() => {
        context.commit('increate', payload)
      }, 2000)
    }
  },
  modules: {
    products,
    cart
  }
})

获取 module 中的状态,主要是通过 $store.state.模块名

products: {{ $store.state.products.products }}

调用模块的 mutation 也是通过 commit ,只不过第一个参数是模块名,第二个参数是数组,数组的内容包括需要调用的 mutation 的方法名和传的参数

<button @click="$store.commit('setProducts', [])">Mutation</button>

在模块中开启命名空间后 namespaced: true ,就可以通过 mapState, mapGetters, mapMutations, mapActions 这些方法来简化试图对模块的调用

computed: {
    ...mapState('products', ['products'])
},
methods: {
    ...mapMutations('products', ['setProducts'])
}

四、模拟实现 VueX

4-1 VueX 基本结构

myvuex 使用

import Vue from 'vue'
import Vuex from '../myvuex'

// 使用 Vue.use() 注册插件,Vue.use() 内部会调用 Vuex 的 install 方法
Vue.use(Vuex)

// 调用 Vuex 的 Store 方法,Store 是一个类,接受一个对象,对象属性包含 state、getters、mutations、actions ......
// 所以说 Vuex 对象中有一个 install 方法和一个 Store 类
export default new Vuex.Store({
  state: {
    count: 0,
    msg: 'Hello World'
  },
  getters: {
    reverseMsg(state) {
      return state.msg.split('').reverse().join('')
    }
  },
  mutations: {
    increate(state, payload) {
      state.count += payload
    }
  },
  actions: {
    increateAsync(context, payload) {
      setTimeout(() => {
        context.commit('increate', payload)
      }, 2000)
    }
  }
})

  1. 首先要明白 VueX 是个 Vue 的插件,所以要使用 Vue.use() 注册插件,Vue.use() 内部会调用 Vuex 的 install 方法
  2. 调用 VueX 的 Store 方法会生成一个 Store 实例,所以说 Store 是一个类,接受一个对象,对象属性包含 state、getters、mutations、actions ......
  3. 综上所述,Vuex 对象中有一个 install 方法和一个 Store 类 myvuex/index.js
let _Vue = null // _Vue 主要用来存储 Vue 构造函数
class Store {

}

// install 方法接收两个参数,一个是 Vue 构造函数,另外一个是额外的选项
function install(Vue) {
  _Vue = Vue
}

export default {
  Store,
  install
}

4-2 实现 VueX 的 install 方法

install 方法的主要作用在 Vue 创建根实例的时候将根实例上的 store 方法注入到 Vue 构造函数的原型上,这样就可以使得所有组件可以通过 this.$store 来获取到 vuex 的 store 仓库

let _Vue = null // _Vue 主要用来存储 Vue 构造函数
class Store {

}

// install 方法接收两个参数,一个是 Vue 构造函数,另外一个是额外的选项
function install(Vue) {
 _Vue = Vue
  _Vue.mixin({
    // 通过 beforeCreate 来获取 vue 实例
    beforeCreate() {
      // 判断 vue 实例上的 $options 是否有 store,如果是组件实例的话,是没有 store 的,只有当创建根实例的时候才会把 store 注入到 Vue 原型上
      if (this.$options.store) {
        // 把创建 store 时传入的 store 对象注入到 Vue 原型上,这样就可以使得所有组件直接通过 this.$store 来获取到 vuex 的 store 仓库
        _Vue.prototype.$store = this.$options.store
      }
    }
  })
}

export default {
  Store,
  install
}

4-2 实现 VueX 的 Store 类

实现 Store 类四步

  1. 实现构造函数,接收 options
  2. state 的响应化处理
  3. getter 的实现
  4. commit、dispatch 方法
let _Vue = null // _Vue 主要用来存储 Vue 构造函数
class Store {
  constructor(options) {
    // 解构
    const {
      state = {},
      getters = {},
      mutations = {},
      actions = {}
    } = options
    // 使用 Vue 的 observable 对 state 做响应化处理
    this.state = _Vue.observable(state)
    // getters 是一个对象,对象中的每一个方法都需要接收 state 作为参数,并且每一个方法都有返回值
    // 说白了 getters 的方法就是对 state 中的值做一些简单处理后返回
    this.getters = Object.create(null) 
    Object.keys(getters).forEach(key => {
      Object.defineProperty(this.getters, key, {
        get: () => getters[key](state)
      })
    })
    this._mutations = mutations
    this._actions = actions
  }

  // 提交 mutations
  commit(type, payload) {
    this._mutations[type](this.state, payload)
  }

  // 分发 actions
  dispatch(type, payload) {
    this._actions[type](this, payload)
  }
}

// install 方法接收两个参数,一个是 Vue 构造函数,另外一个是额外的选项
function install(Vue) {
  _Vue = Vue
  _Vue.mixin({
    // 通过 beforeCreate 来获取 vue 实例
    beforeCreate() {
      // 判断 vue 实例上的 $options 是否有 store,如果是组件实例的话,是没有 store 的,只有当创建根实例的时候才会把 store 注入到 Vue 原型上
      if (this.$options.store) {
        // 把创建 store 时传入的 store 对象注入到 Vue 原型上,这样就可以使得所有组件直接通过 this.$store 来获取到 vuex 的 store 仓库
        _Vue.prototype.$store = this.$options.store
      }
    }
  })
}

export default {
  Store,
  install
}