状态管理学习(二)Vuex、简单模拟Vuex

Vuex状态管理精讲
本文深入解析Vuex,Vue.js的状态管理库,探讨其核心概念如Store、State、Getter、Mutation、Action和Module,以及如何在大型单页应用中有效管理和共享组件状态。

Vuex 概念回顾

什么是 Vuex

Vuex 官网

  • Vuex 是专门为 Vue.js 设计的状态管理库(JS库)
  • Vuex 采用集中式的方式存储需要共享的状态,并以相应的规则保证状态以一种可预测的方式发生变化。
    • 相比简单 store 模式 在状态过多时不宜管理
    • Vuex 提供了一种模块的机制,可以按模块划分不同功能的状态
  • Vuex 的作用是进行状态管理,解决复杂组件通信,数据共享
  • Vuex 集成到了Vue的官方调试工具 devtools extension 中,提供了 time-travel 时光旅行和历史回滚功能等功能

什么情况下使用 Vuex

  • 非必要的情况不要使用 Vuex
    • 如果项目不大,并且组件间状态共享不多的情况下,使用 Vuex 的益处并没有付出的时间多
    • 此时使用简单的 store 模式,或其他方式就可以满足需求
  • 建议大型的单页应用程序中使用 Vuex,可以更好的管理组件间共享的状态
    • 多个视图依赖同一状态
    • 来自不同试图的行为需要变更同一状态

官方文档:

Vuex 可以帮助我们管理共享状态,并附带了更多的概念和框架。这需要对短期和长期效益进行权衡。

如果您不打算开发大型单页应用,使用 Vuex 可能是繁琐冗余的。确实是如此——如果您的应用够简单,您最好不要使用 Vuex。一个简单的 store 模式 就足够您所需了。但是,如果您需要构建一个中大型单页应用,您很可能会考虑如何更好地在组件外部管理状态,Vuex 将会成为自然而然的选择。引用 Redux 的作者 Dan Abramov 的话说就是:

Flux 架构就像眼镜:您自会知道什么时候需要它。

Vuex 核心概念

在这里插入图片描述

流程:

  • state - 管理的全局状态
  • Vue Components - 把状态绑定(渲染 Render)到组件(视图)展示给用户
  • 用户通过 Dispatch 分发 Action
    • 可以执行异步请求 Backend API
  • 请求完 Commit(提交)Mutation,改变状态的更改并记录

核心概念:

  • Store - 仓库
    • Store 是使用Vuex应用程序的核心
    • 每个应用仅有1个 Store
    • Store 是一个容器,包含应用中的大部分状态
    • 不能直接改变 Store 中的状态,要通过提交 Mutation 的方式改变状态
  • State - 状态
    • 状态保存在 Store 中,并且是响应式的
    • 因为 Store 是唯一的,所以状态也是唯一的,称为单一状态树
    • 但是所有的状态都保存在 State 中的话,会让程序难以维护,可以通 过Module (模块)解决该问题
  • Getter - 类似计算属性
    • 方便从一个属性派生其他的值
    • 内部可以对计算的结果进行缓存
    • 只有当依赖的状态发生改变的时候,才会进行计算
  • Mutation - 通过提交 Mutation 改变状态
    • Mutations 是同步的
    • 所有状态的变化必须要通过提交 Mutation 来完成,目的是:
      • 可以通过 Mutations 追踪到数据的变化
      • 阅读代码是,更容易分析应用内部的状态改变
      • 还可以记录每次状态的改变,实现高级调试功能,例如:time travel 和 历史回滚
  • Action - 和 Mutation 类似,也是用于改变状态
    • 不同的是 Action 可以进行异步的操作
    • 内部改变状态的时候,还是要提交 Mutation
  • Module - 模块
    • 由于使用单一状态树,应用的所有状态会集中到一个比较大的状态上来
    • 当应用变得复杂时,Store 对象就有可能变得相当臃肿
    • 为了解决以上问题,Vuex 允许将 Store 分割成模块
    • 每个模块拥有自己的 State Mutation Action Getter,甚至是嵌套的子模块

Vuex 基本结构

使用Vue CLI 创建项目的时候,如果选择了 Vuex,会自动生成 Vuex 代码的基本结构。

// src/store/index.js
import Vue from 'vue'
// 导入 Vuex
import Vuex from 'vuex'

// 注册插件
Vue.use(Vuex)

// 创建并导出 Vuex中的 Store 对象并导出
export default new Vuex.Store({
  state: {
  },
  mutations: {
  },
  actions: {
  },
  modules: {
  }
  // getters
})

// src/main.js
import Vue from 'vue'
import App from './App.vue'
import router from './router'
// 导入 store 对象
import store from './store'

Vue.config.productionTip = false

new Vue({
  router,
  // 创建 Vue 实例时传入 store 选项
  // store 选项会被注入到 Vue 实例中:this.$store
  store,
  render: h => h(App)
}).$mount('#app')

State

State 是单一状态树,用一个对象存储了全部的应用层级状态(所有的状态数据)。

并且 State 是响应式的。

在仓库(Store)中设置的状态都可以在组件中直接使用,获取数据时,直接从 State 中获取。

// src/store/index.js
import Vue from 'vue'
import Vuex from 'vuex'

Vue.use(Vuex)

export default new Vuex.Store({
  state: {
    count: 0,
    msg: 'Hello Vuex'
  },
  mutations: {
  },
  actions: {
  },
  modules: {
  }
})
// src/app.vue
<template>
  <div id="app">
    <h1>Vuex - demo</h1>
    count:{{$store.state.count}}<br/>
    msg:{{$store.state.msg}}
  </div>
</template>

Vuex 内部提供了 mapState 函数,自动生成状态对应的计算属性。

// src/app.vue
<template>
  <div id="app">
    <h1>Vuex - demo</h1>
    <!-- count:{{ $store.state.count }}<br />
    msg:{{ $store.state.msg }} -->

    count:{{ count }}<br />
    msg:{{ msg }}
  </div>
</template>

<script>
import { mapState } from 'vuex'
export default {
  computed: {
    // 接收一个数组作为参数
    // 数组中存放要映射的属性名
    // 返回一个包含两个计算属性方法的对象
    // return {
    //   count: state => state.count,
    //   msg: state => state.msh
    // }
    ...mapState(['count', 'msg'])
  }
}
</script>

<style></style>

  • 使用 mapState 可以让视图中的代码更简洁。
  • 但是如果组件中已经有 count 或 msg 属性,再使用 mapState 就会造成命名冲突。

mapState 还可以接收对象作为参数,使用对象方式可以修改生成的计算属性的名称(解决命名冲突):

// src/app.vue
<template>
  <div id="app">
    <h1>Vuex - demo</h1>

    count:{{ num }}<br />
    msg:{{ message }}
  </div>
</template>

<script>
import { mapState } from 'vuex'
export default {
  computed: {
    // 接收一个对象作为参数
    // 可以修改生成的计算属性的名称
    // key 为最终生成的计算属性的名称
    // value 是映射的属性的名称
    ...mapState({
      num: 'count',
      message: 'msg'
    })
  }
}
</script>

<style></style>

Getter

Vuex 中的 Getter 类似计算属性,可以对 state 中的数据进行处理再展示。

  • 接收 state 作为参数
  • 与计算属性一样,最终返回属性处理后的值
// src/store/index.js
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: {
  }
})

// src/app.vue
<template>
  <div id="app">
    <h1>Vuex - demo</h1>
    
    count:{{ num }}<br />
    msg:{{ message }}
    
    <h2>Getter</h2>
    reverseMsg:{{$store.getters.reverseMsg}}
  </div>
</template>

<script>
import { mapState } from 'vuex'
export default {
  computed: {
    ...mapState({
      num: 'count',
      message: 'msg'
    })
  }
}
</script>

它同样有简化的函数:mapGetters,使用和 mapState 类似。

  • 它用于把 Vuex 中的 getter 映射到组件中的计算属性。
  • 同样接收数组和对象,使用和 mapState 一样
  • 同样返回一个数组
// src/app.vue
<template>
  <div id="app">
    <h1>Vuex - demo</h1>
    
    count:{{ num }}<br />
    msg:{{ message }}
    
    <h2>Getter</h2>
    <!-- reverseMsg:{{$store.getters.reverseMsg}} -->
    reverseMsg:{{reverseMsg}}
  </div>
</template>

<script>
import { mapState, mapGetters } from 'vuex'
export default {
  computed: {
    ...mapState({
      num: 'count',
      message: 'msg'
    }),

    ...mapGetters(['reverseMsg'])
  }
}
</script>

Mutation

Vuex 约定,更改 store 中状态的唯一方法是提交 Mutation,目的是方便在 devtools 中调试。

  • Mutation 必须是同步执行的,这样可以保证能够在 Mutation 中收集到所有的状态修改。

  • 不要在 Mutation 中执行异步操作修改 State,否则调试工具无法正常的观测到数据的变化。

  • 如果想要执行异步的操作,需要使用 Action。

Vuex 中的mutation非常类似于事件:每个mutation都有一个字符串的 事件类型(type) 和一个 回调函数(handler)

这个回调函数就是我们实际进行状态更改的地方,它接收两个参数:

  • state - 状态
  • payload - 载荷,是调用 Mutation 时传递的额外参数
    • 多个参数可以包装到一个对象中传递

调用 Store 中的 Mutation 类似于事件,它需要通过 $store.commit 提交,它接收:

  • Muataion 的名字作为第一个参数,相当于事件的名称
  • 还可以接收第二个参数用于传递额外的参数,即 Mutation 的载荷 payload

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

可以通过 mapMutations 把 Mutation 映射到组件的 methods 中,mapMutations 返回的方法封装了 commit 的调用。

// src/store/index.js
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: {}
})

// src/app.vue
<template>
  <div id="app">
    <h1>Vuex - demo</h1>
    count:{{ num }}<br />
    msg:{{ message }}

    <h2>Getter</h2>
    reverseMsg:{{reverseMsg}}

    <h2>Mutation</h2>
    <!-- <button @click="$store.commit('increate', 2)">Mutation</button> -->
    <button @click="increate(2)">Mutation</button>
  </div>
</template>

<script>
import { mapState, mapGetters, mapMutations } from 'vuex'
export default {
  computed: {
    ...mapState({
      num: 'count',
      message: 'msg'
    }),

    ...mapGetters(['reverseMsg'])
  },
  methods: {
    ...mapMutations(['increate'])
  }
}
</script>

Vue devtools 查看时光旅行和历史回滚

打开安装好的 chrome 浏览器插件: vue devtools,查看 Vuex:

在这里插入图片描述

  • 左侧是 mutations 提交记录
    • Base State 初始 state
  • 右侧是 本次提交的 mutation 、state、getters

mutation 每次提交记录中提供3个操作,目的是为了方便调试,从右向左依次是:

  • Time Travel to This State - 时光旅行
    • 点击会跳转到本次 mutation 提交的时候
  • Revert This Mutation - 历史回滚
    • 回滚到本次提交之前,点击后,本次和之后的提交都被清空
  • Commit This Mutation - 把本次提交,作为最后一次提交
    • 点击后,会把本次记录重置为最后一次提交的记录,并向上重置,直到 Base State,清空没被重置的记录

Action

在Action中可以执行异步操作,在异步操作之后,如果需要修改状态,可以通过提交 Mutation 来修改 State(所有的状态更改都要通过 Mutation)。

Action 接收两个参数:

  • context - 上下文,包含 state、getters、commit、dispatch 等属性
  • payload - 额外的参数

调用 Store 中的 Action 和 通过 commit 调用 Mutation 一样,它需要通过 $store.dispatch 提交,它接收:

  • Action 的名字作为第一个参数
  • 第二个参数用于传递额外的参数,即 payload

可以通过 mapActions 把 Action 映射到组件的 methods 中,mapActions 返回的方法封装了 dispatch的调用。

// src/store/index.js
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)
      }, 1000)
    }
  },
  modules: {}
})

// src/app.vue
<template>
  <div id="app">
    <h1>Vuex - demo</h1>

    count:{{ num }}<br />
    msg:{{ message }}

    <h2>Getter</h2>
    reverseMsg:{{reverseMsg}}

    <h2>Mutation</h2>
    <button @click="increate(2)">Mutation</button>

    <h2>Action</h2>
    <!-- <button @click="$store.dispatch('increateAsync', 3)">Action</button> -->
    <button @click="increateAsync(3)">Action</button>
  </div>
</template>

<script>
import { mapState, mapGetters, mapMutations, mapActions } from 'vuex'
export default {
  computed: {
    ...mapState({
      num: 'count',
      message: 'msg'
    }),

    ...mapGetters(['reverseMsg'])
  },
  methods: {
    ...mapMutations(['increate']),
    ...mapActions(['increateAsync'])
  }
}
</script>

Module

由于使用单一状态树,应用的所有状态会集中到一个比较大的对象。

当应用变得非常复杂时,store 对象就有可能变得非常臃肿。

Vuex 可以把 单一状态树拆(store )分成多个模块(module)。

每个模块都可以拥有自己的 state、mutations、actions、getters,甚至嵌套子模块。

定义模块

  • 模块中只是定义并导出了相关的成员。
// src/store/products.js
const state = {
  products: [
    { id: 1, title: 'iPhone 11', price: 8000 },
    { id: 1, title: 'iPhone 12', price: 10000 }
  ]
}

const getters = {
  productCount (state) {
    return state.products.length
  }
}

const mutations = {
  setProducts (state, payload) {
    state.products = payload
  }
}

const actions = {}

export default {
  state,
  getters,
  mutations,
  actions
}

// src/store/cart.js
const state = {}

const getters = {}

const mutations = {}

const actions = {}

export default {
  state,
  getters,
  mutations,
  actions
}

注册模块

  • 导入模块
  • 在 modules 中注册模块
// src/store/index.js
import Vue from 'vue'
import Vuex from 'vuex'
// 导入模块
import products from './products'
import cart from './cart'

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)
      }, 1000)
    }
  },
  // 注册模块
  modules: {
    products,
    cart
  }
})

注册完(可以打印 store 看看):

  • 会把模块中的状态挂载到 $store.state 中
    • 可以通过 $store.state.[模块名].[模块中state的属性名] 方法
  • 把模块的 mutations、actions 记录到 $store 的内部属性 _mutations_actions
    • _mutations_actions 以对象形式存放 Store 中所有的 mutations 和 actions
      • key 是 mutations / actions 的名称
      • value 是所有 mutations / actions 同名的方法组成的数组
    • 可以通过 $store.commit 直接提交模块中的 mutation
    • 可以通过 $store.dispatch直接提交模块中的 action
  • 把模块的 getters 记录到 $store 的 getters 中
    • 它没有根据模块分类(类似state),也没有收集所有方法(类似 mutations / actions),它是平铺存储,注意避免命名冲突
// src/app.vue
<template>
  <div id="app">
    <h1>Vuex - demo</h1>
    <h2>Module</h2>
    products:{{$store.state.products.products}}<br/>
    products count:{{$store.getters.productCount}}<br/>
    <button @click="$store.commit('setProducts', [])">Mutation</button>
  </div>
</template>

当前所演示的模块中的 mutations、actions、getters 都直接存储在 Store 中。

如果想要模块有更好的封装度和复用性,可以给模块开启命名空间。

将来在视图中使用模块中的成员的时候,看起来更清晰一些。

模块命名空间

推荐使用命名空间的用法。

在模块导出的对象中,通过namespacing 开启命名空间。

// src/store/cart.js
const state = {}

const getters = {}

const mutations = {}

const actions = {}

export default {
  // 开启命名空间
  namespaced: true,

  state,
  getters,
  mutations,
  actions
}

// src/store/products.js 一样

打印 Store 对比,发现 mutations、actions、getters 中模块的成员名都添加了模块名/作为前缀:

在这里插入图片描述

在视图中通过 mapXxxx 方法映射模块的成员:

  • 第一个参数是模块命名空间的名字,也就是在 Store 的 modules 中定义的模块的名字。
  • 第二个参数依然是数组或对象
// src/app.vue
<template>
  <div id="app">
    <h1>Vuex - demo</h1>
    <h2>Module</h2>
    <!-- products:{{$store.state.products.products}}<br/>
    products count:{{$store.getters.productCount}}<br/>
    <button @click="$store.commit('setProducts', [])">Mutation</button> -->

    products:{{products}}<br/>
    products count:{{productCount}}<br/>
    <button @click="setProducts([])">Mutation</button>
  </div>
</template>

<script>
import { mapState, mapGetters, mapMutations, mapActions } from 'vuex'
export default {
  computed: {
    // 模块的成员
    ...mapState('products', ['products']),
    ...mapGetters('products', ['productCount'])
  },
  methods: {
    ...mapMutations('products', ['setProducts'])

  }
}
</script>

Vuex 严格模式

Vuex 约定所有状态的变更都应该通过提交 Mutation。

但是语法上,可以直接访问和修改 $store.state 中的属性。

这破坏了Vuex的约定。

如果在组件中直接修改 state,devtools 无法跟踪到状态的修改。

开启严格模式之后,在组件中直接修改 state ,虽然还是会生效,但是会抛出错误:

[vuex] do not mutate vuex store state outside mutation handlers.
# 不要在 mutation 之外修改 vuex 中的状态

在创建 Store 的时候通过 strict 选项开启:

// 创建并导出 Vuex中的 Store 对象并导出
export default new Vuex.Store({
  strict: true,
  // ...
})
<button @click="$store.state.msg = 'Yes'">strict</button>
{{msg}}

注意:不要在生产环境开启严格模式。

严格模式会深度检查状态树,检查不合规的状态改变,会影响性能

可以在开发环境启用严格模式,在生产环境关闭严格模式。

export default new Vuex.Store({
  // 打包时根据环境变量进行处理
  // npm run serve 时 NODE_ENV = development
  // npm run build 时 NODE_ENV = production
  strict: process.env.NODE_ENV !== 'production'
)}

Vuex 的插件

以购物车为例,用户的购物车商品信息是记录在本地 localStorage 中,以保证刷新页面,还能保留记录。

这样就需要在每个修改(增、删等)购物车商品信息的 mutation 中更新 localStorage 中的数据。

可以使用 Vuex 的插件简化这个操作。

  • Vuex 的插件就是要给函数
  • 这个函数接收一个 store 的参数
  • 这个函数中可以注册(store.subscribe)一个函数,可以在每个 mutation 结束后执行
    • subscribe 用于订阅 mutation
    • 它注册的回调函数,会在每个 mutation 完成之后调用
  • 通过 store 的 plugins 选项注册
  • 插件要在创建 store 之前创建

例如:

const myPlugin = store => {
  // 当 store 初始化后调用
  store.subscribe((mutation, state) => {
    // subscribe 用于订阅 mutation
    // 注册的回调函数,会在每个 mutation 完成之后调用
    // mutaion -> {type, payload}
    //   type:'命名空间/mutaion的名字'
    //   payload:mutation接收的参数
    // state:store的state,不是模块的
  })
}

模拟简单的 Vuex

Vuex 基本结构

Vuex 模块包含:

  • Store 类 - 用于实例化 Store
  • install 函数 - 用于 Vue.use 注册 Vuex 插件
// src/myvuex/index.js
let _Vue = null

class Store {}

function install (Vue) {
  _Vue = Vue
}

export default {
  Store,
  install
}

install

install 的工作是把创建Vue实例时传入的 store 对象,注入到Vue原型上的$store。

在所有组件中都可以通过 this.$store 获取到Vuex中的仓库,从而共享状态。

let _Vue = null
function install (Vue) {
  _Vue = Vue
  // 在install中获取不到Vue的实例
  // 所以需要混入 beforeCreate 来获取Vue实例
  // 从而拿到选项中的 store 对象
  _Vue.mixin({
    beforeCreate () {
      // 判断$options中是否有store选项
      // 组件实例没有store,不需要处理
      if (this.$options.store) {
        _Vue.prototype.$store = this.$options.store
      }
      
    }
  })
}

Store 类

  • 构造函数 - 接收一个选项对象
  • 属性:
    • state - 响应式的
    • getters
    • mutations
    • actions
  • 方法:
    • commit - 提交 mutation
    • dispatch - 分发 action

初始化:

  • this.state - 初始化为选项中传入的 state 进行响应式处理后的对象
  • this.getters
    • 选项传入的 getters 是一个对象,对象属性的值都是一个方法
    • 这些方法都接收一个 state 参数,并最终都有返回值
    • 一般情况下就是对状态作简单的处理,把结果返回
    • 这些方法的作用都是当访问getters中的成员的时候,去获取值
    • 可以把这些方法,通过 Object.defineProperty 转换成 this.getters 对象中的 get 访问器
    • 并将 state 传入
  • this._mutations && this._actions - 直接存储 选项传入的对象
    • 它们是内部属性,在commit 或 dispatch 方法中获取
    • 在它们的名称前加上下划线前缀表示私有,不希望外部访问
  • commit
    • 接收两个参数
      • type - this._mutations中方法的名字
      • payload - 调用方法时传入的参数
    • 内部调用 mutation 的方法,并传入:
      • state - 当前实例的state
      • payload
  • dispatch
    • 参数与 commit 类似
    • 内部调用 action 的方法,并传入:
      • context - 上下文,也就是当前实例
      • payload - 调用方法时传入的参数
class Store {
  constructor(options) {
    const { state = {}, getters = {}, mutations = {}, actions = {} } = options
    // 对 state 进行响应式处理
    this.state = _Vue.observable(state)

    // this.getters 不需要原型
    this.getters = Object.create(null)
    // 将 getters 中的方法转换成 this.getters 对象的get 范文其
    Object.keys(getters).forEach(key => {
      Object.defineProperty(this.getters, key, {
        // 获取 getters 中方法的执行结果
        get: () => getters[key](state)
      })
    })

    // mutations和actions 在commit 或 dispatch 方法中获取
    // 所以它们应该是内部私有成员,下划线前缀标识私有,不希望外部访问
    this._mutations = mutations
    this._actions = actions
  }

  commit (type, payload) {
    this._mutations[type](this.state, payload)
  }

  dispatch (type, payload) {
    this._actions[type](this, payload)
  }
}

到此Vuex模拟完成,替换vuex的模块地址为 myvuex的地址,可以测试效果。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值