一、概要

1.1、Vuex定义与注意事项

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

使用Vue开发中需要将应用拆分成多个组件,但是组件与组件之间数据共享成了一个问题,父子组件实现起来相对简单,有很多兄弟组件和跨多级组件,实现起来过程繁琐,在多人协同开发上,不利于统一管理,Vuex可以解决这些问题。

1.1.1、状态管理模式

没有使用Vuex时,让我们看一个简单的 Vue 计数应用:

  1. new Vue({
  2. // state
  3. data () {
  4. return {
  5. count: 0
  6. }
  7. },
  8. // view
  9. template:'<div>{{ count }}</div>',
  10. // actions
  11. methods: {
  12. increment () {
  13. this.count++
  14. }
  15. }
  16. })

这个状态自管理应用包含以下几个部分:

  • state,驱动应用的数据源;
  • view,以声明方式将 state 映射到视图;
  • actions,响应在 view 上的用户输入导致的状态变化。

以下是一个表示“单向数据流”理念的极简示意:

但是,当我们的应用遇到多个组件共享状态时,单向数据流的简洁性很容易被破坏:

  • 多个视图依赖于同一状态。
  • 来自不同视图的行为需要变更同一状态。

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

因此使用Vuex,我们为什么不把组件的共享状态抽取出来,以一个全局单例模式管理呢?在这种模式下,我们的组件树构成了一个巨大的“视图”,不管在树的哪个位置,任何组件都能获取状态或者触发行为!

另外,通过定义和隔离状态管理中的各种概念并强制遵守一定的规则,我们的代码将会变得更结构化且易维护。

这就是 Vuex 背后的基本思想,借鉴了  FluxRedux、和  The Elm Architecture。与其他模式不同的是,Vuex 是专门为 Vue.js 设计的状态管理库,以利用 Vue.js 的细粒度数据响应机制来进行高效的状态更新。

1.1.2、使用 Vuex

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

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

1.1.3、注意事项

Vuex会有一定的门槛和复杂性,它的主要使用场景是大型单页面应用,如果你的项目不是很复杂,用一个bus也可以实现数据的共享(在前面讲组件的内容中已经讲到过bus作为总线进行通信的示例),但是它在数据管理,维护,还只是一个简单的组件,而Vuex可以更优雅高效地完成状态管理,所以,是否使用Vuex取决于你的团队和技术储备。

使用bus作为总线通信的示例:

  1. <!DOCTYPE html>
  2. <html>
  3. <head>
  4. <meta charset="UTF-8">
  5. <title>Vue2 Demo</title>
  6. </head>
  7. <body>
  8. <div id="app01">
  9. <my-comp1></my-comp1>
  10. <my-comp2></my-comp2>
  11. </div>
  12. <script src="../../js/vue/vue.js"></script>
  13. <script>
  14. //事件总线
  15. var bus = new Vue();
  16.  
  17. Vue.component("my-comp1", {
  18. template: "<button @click='incrN'>{{n}}</button>",
  19. data() {
  20. return {n: 0}
  21. },
  22. methods: {
  23. incrN: function () {
  24. this.n++;
  25. //发布事件
  26. bus.$emit("inc",this.n);
  27. }
  28. }
  29. });
  30.  
  31. Vue.component("my-comp2", {
  32. template: "<button @click='incrN'>{{n}}</button>",
  33. data() {
  34. return {n: 999}
  35. },
  36. methods: {
  37. incrN: function () {
  38. this.n--;
  39. }
  40. },
  41. //勾子,挂载完成时执行事件
  42. mounted:function () {
  43. var _this=this;
  44. //监听事件,订阅事件
  45. bus.$on("inc",function (val) {
  46. _this.n+=val;
  47. })
  48. }
  49. });
  50.  
  51. var vm = new Vue({
  52. el: "#app01",
  53. data: {}
  54. });
  55. </script>
  56. </body>
  57. </html>

1.2、概念

每一个 Vuex 应用的核心就是store(仓库),store基本上就是一个容器,它包含着你的应用中大部分的状态 (state)。 Vuex和单纯的全局对象有以下两点不同:

1.Vuex 的状态存储是响应式的。当 Vue 组件从 store 中读取状态的时候,若 store 中的状态发生变化,那么相应的组件也会相应地得到高效更新;

2.你不能直接改变 store 中的状态。改变 store 中的状态的唯一途径就是显式地提交 (commit) mutation。这样使得我们可以方便地跟踪每一个状态的变化,从而让我们能够实现一些工具帮助我们更好地了解我们的应用。

  • store:表示对Vuex对象的全局引用。组件通过Store来访问Vuex对象中的State。
  • state:保存数据的状态、对象的状态,即其所拥有的数据。
  • getter:相当于Store的计算属性。因为就像计算属性一样,Getter的返回值会根据它的依赖被缓存起来,且只有当它的依赖值发生了改变才会被重新计算。下面会说到具体的使用场景。
  • mutations:定义了对state中数据的修改操作,更改store中的状态的唯一方法是提交mutation。mutation类似于事件:每个mutation都有一个字符串的事 件类型(type),和一个回调函数(handler).利用store.commit('方法名')来调用这个函数。
  • mutations-type:可以认为是store中的计算属性,mapGetters是辅助函数,仅仅将store中的getter映射到局部计算属性。
  • action :mutation中定义的操作只能执行同步操作,Vuex中的异步操作在Action中进行,Action最终通过调用Mutation的操作来更新数据;类似于mutation,不同在于action提交的是mutation,而不是直接变更状态,action可以包含任意 异步操作。action用store.dispatch方法触发函数。mapActions是辅助函数,将组件的 methods 映射为store.dispatch。
  • module:Store和State之间的一层,便于大型项目管理,Store包含多个Module,Module包含State、Mutation和Action。

1.3、资源

github: https://github.com/vuejs/vuex

中文帮助: https://vuex.vuejs.org/zh/(本文大量引用)

英文帮助: https://vuex.vuejs.org/

视频教程: https://www.bilibili.com/video/av17503637/

二、安装

2.1、直接下载或CDN 引用

引用地址: https://unpkg.com/vuex

Unpkg.com 提供了基于 NPM 的 CDN 链接。以上的链接会一直指向 NPM 上发布的最新版本。您也可以通过 https://unpkg.com/vuex@3.0.1/dist/vuex.js这样的方式指定特定的版本。

CDN引用:

  1. <script src="https://unpkg.com/vue"></script>
  2. <script src="https://unpkg.com/vuex"></script>

也可以使用下载后本地引用,vuex的github地址是: https://github.com/vuejs/vuex

2.2、NPM包管理器

  1. npm i vuex --save

2.3、Yarn

  1. yarn add vuex

在一个模块化的打包系统中,您必须显式地通过 Vue.use() 来安装 Vuex:

  1. import Vue from 'vue'
  2. import Vuex from 'vuex'
  3.  
  4. Vue.use(Vuex)
  5.  
  6. //当使用全局 script 标签引用 Vuex 时,不需要以上安装过程。

2.4、Promise

Vuex 依赖 Promise。如果你支持的浏览器并没有实现 Promise (比如 IE),那么你可以使用一个polyfill的库,例如 es6-promise。

你可以通过 CDN 将其引入:

  1. <script src="https://cdn.jsdelivr.net/npm/es6-promise@4/dist/es6-promise.auto.js"></script>

然后 window.Promise 会自动可用。

如果你喜欢使用诸如 npm 或 Yarn 等包管理器,可以按照下列方式执行安装:

  1. npm install es6-promise --save # npm
  2. yarn add es6-promise # Yarn

或者更进一步,将下列代码添加到你使用 Vuex 之前的一个地方:

  1. import 'es6-promise/auto'

2.5、自己构建

如果需要使用 dev 分支下的最新版本,您可以直接从 GitHub 上克隆代码并自己构建。

  1. git clone https://github.com/vuejs/vuex.git node_modules/vuex
  2. cd node_modules/vuex
  3. npm install
  4. npm run build

三、应用

3.1、页面使用Vuex快速起步

创建一个 store,创建过程直截了当——仅需要提供一个初始 state 对象和一些 mutation:

  1. // 如果在模块化构建系统中,请确保在开头调用了 Vue.use(Vuex)
  2. const store = new Vuex.Store({
  3. state: {
  4. count: 0
  5. },
  6. mutations: {
  7. increment (state) {
  8. state.count++
  9. }
  10. }
  11. })

现在,你可以通过 store.state 来获取状态对象,以及通过 store.commit 方法触发状态变更:

  1. store.commit('increment')
  2.  
  3. console.log(store.state.count) // -> 1

再次强调,我们通过提交 mutation 的方式,而非直接改变 store.state.count,是因为我们想要更明确地追踪到状态的变化。这个简单的约定能够让你的意图更加明显,这样你在阅读代码的时候能更容易地解读应用内部的状态改变。此外,这样也让我们有机会去实现一些能记录每次状态改变,保存状态快照的调试工具。有了它,我们甚至可以实现如时间穿梭般的调试体验。

由于 store 中的状态是响应式的,在组件中调用 store 中的状态简单到仅需要在计算属性中返回即可。触发变化也仅仅是在组件的 methods 中提交 mutation。

完整示例代码:

  1. <!DOCTYPE html>
  2. <html lang="en">
  3. <head>
  4. <meta charset="UTF-8">
  5. <title>Title</title>
  6. </head>
  7. <body>
  8. <div id="app">
  9. <p>{{ count }}</p>
  10. <p>
  11. <button @click="increment">+</button>
  12. <button @click="decrement">-</button>
  13. </p>
  14. <div>
  15. <comp1></comp1>
  16. <comp1></comp1>
  17. </div>
  18. </div>
  19. <script src="https://unpkg.com/vue"></script>
  20. <script src="https://unpkg.com/vuex"></script>
  21. <script>
  22.  
  23. //定义仓库对象
  24. const store = new Vuex.Store({
  25. state: {
  26. count: 0
  27. },
  28. mutations: {
  29. increment: state => state.count++,
  30. decrement: state => state.count--
  31. }
  32. })
  33.  
  34. //定义组件
  35. Vue.component("comp1",{
  36. template:"<h2>{{count}}</h2>",
  37. computed: {
  38. count () {
  39. return store.state.count
  40. }
  41. }
  42. });
  43.  
  44. new Vue({
  45. el: '#app',
  46. computed: {
  47. count () {
  48. return store.state.count
  49. }
  50. },
  51. methods: {
  52. increment () {
  53. store.commit('increment')
  54. },
  55. decrement () {
  56. store.commit('decrement')
  57. }
  58. }
  59. })
  60. </script>
  61. </body>
  62. </html>

运行结果:

在线示例:这是一个 最基本的 Vuex 记数应用示例

3.2、Vue-cli中使用Vuex快速起步

安装vuex:

插件引用:

  1. //导入插件
  2. import Vuex from 'vuex'
  3.  
  4. //使用插件
  5. Vue.use( Vuex );
  6.  
  7. //定义仓库对象
  8. const store = new Vuex.Store({
  9. //属性
  10. })
  11.  
  12. //定义vue实例并关联存储仓库
  13. new Vue({
  14. el: '#app',
  15. store,
  16. render: h => h(App)
  17. });

定义状态对象main.js:

  1. import Vue from 'vue'
  2. import Vuex from 'vuex';
  3. import App from './App'
  4. import router from './router/hello'
  5.  
  6. Vue.config.productionTip = false
  7.  
  8. Vue.use(Vuex);
  9.  
  10. //定义仓库对象
  11. const store = new Vuex.Store({
  12. state: {
  13. count: 0
  14. },
  15. mutations: {
  16. increment: state => state.count++,
  17. decrement: state => state.count--
  18. }
  19. })
  20.  
  21. /* eslint-disable no-new */
  22. new Vue({
  23. el: '#app',
  24. router,
  25. store,
  26. components: { App },
  27. render:r=>r(App)
  28. })

直接使用状态对象中的数据App.vue:

  1. <template>
  2. <div>
  3. <img src="./assets/logo.png">
  4. <h2>
  5. {{$store.state.count}}
  6. </h2>
  7. <Counter/>
  8. <header>
  9. <!-- router-link 定义点击后导航到哪个路径下 -->
  10. <router-link to="/" exact>index</router-link>
  11. <router-link to="/foo">Go to Foo</router-link>
  12. <router-link to="/bar">Go to Bar</router-link>
  13. </header>
  14. <!-- 对应的组件内容渲染到router-view中 -->
  15. <router-view></router-view>
  16. </div>
  17. </template>
  18. <script>
  19. import Counter from './components/Counter';
  20.  
  21. export default {
  22. components:{Counter}
  23. }
  24. </script>
  25. <style scoped>
  26. a {
  27. color: #777;
  28. }
  29.  
  30. a:hover {
  31. color: orangered;
  32. }
  33. </style>

在组件中使用状态对象:

  1. //Counter.vue
  2. <template>
  3. <div id="app">
  4. <h2>{{ count }}</h2>
  5. <p>
  6. <button @click="increment">+</button>
  7. <button @click="decrement">-</button>
  8. </p>
  9. </div>
  10. </template>
  11.  
  12. <script>
  13. export default {
  14. name: "Counter",
  15. computed: {
  16. count() {
  17. return this.$store.state.count
  18. }
  19. },
  20. methods: {
  21. increment() {
  22. this.$store.commit('increment')
  23. },
  24. decrement() {
  25. this.$store.commit('decrement')
  26. }
  27. }
  28. }
  29. </script>
  30.  
  31. <style scoped>
  32. h2 {
  33. color: darkblue;
  34. }
  35. </style>

引用组件:

  1. //bar.vue
  2.  
  3. <template>
  4. <div>
  5. <h2>Bar</h2>
  6. <p>{{msg}}</p>
  7. <Counter/>
  8. </div>
  9. </template>
  10. <script>
  11. import Counter from './Counter';
  12.  
  13. export default {
  14. data() {
  15. return {
  16. msg: "我是Bar组件"
  17. }
  18. },
  19. components: {Counter}
  20. }
  21. </script>
  22. <style scoped>
  23. h2 {
  24. color: springgreen;
  25. }
  26. </style>

App.Vue内容:

  1. <template>
  2. <div>
  3. <img src="./assets/logo.png">
  4. <h2>
  5. {{$store.state.count}}
  6. </h2>
  7. <Counter/>
  8. <header>
  9. <!-- router-link 定义点击后导航到哪个路径下 -->
  10. <router-link to="/" exact>index</router-link>
  11. <router-link to="/foo">Go to Foo</router-link>
  12. <router-link to="/bar">Go to Bar</router-link>
  13. </header>
  14. <!-- 对应的组件内容渲染到router-view中 -->
  15. <router-view></router-view>
  16. </div>
  17. </template>
  18. <script>
  19. import Counter from './components/Counter';
  20.  
  21. export default {
  22. components:{Counter}
  23. }
  24. </script>
  25. <style scoped>
  26. a {
  27. color: #777;
  28. }
  29.  
  30. a:hover {
  31. color: orangered;
  32. }
  33. </style>

运行结果:

切换到一个单页Bar

3.3、没有使用vuex的汽车列表(示例)

为了方便后面的内容讲解这里重新做一个简单的vuex汽车列表示例,这个示例分别有两个组件CarListOne.vue和CarListTwo.vue, 在App.vue的datat中保存着共有的汽车列表, 代码和初始化的效果如下图所示:

App.Vue:
  1. <template>
  2. <div id="app">
  3. <h2>汽车商城</h2>
  4. <hr/>
  5. <car-list-one v-bind:cars="cars"></car-list-one>
  6. <car-list-two v-bind:cars="cars"></car-list-two>
  7. </div>
  8. </template>
  9.  
  10. <script>
  11. import CarListOne from './components/CarListOne.vue'
  12. import CarListTwo from './components/CarListTwo.vue'
  13.  
  14. export default {
  15. name: 'app',
  16. components: {
  17. 'car-list-one': CarListOne,
  18. 'car-list-two': CarListTwo
  19. },
  20. data() {
  21. return {
  22. cars: [
  23. {name: '奇瑞', price: 18.3},
  24. {name: '吉利', price: 19.6},
  25. {name: '长安', price: 17.5},
  26. {name: '红旗', price: 21.9}
  27. ]
  28. }
  29. }
  30. }
  31. </script>
  32.  
  33. <style>
  34. h2 {
  35. color: orangered;
  36. }
  37. </style>

CarListOne.vue

  1. <template>
  2. <div id="car-list-one">
  3. <h2>Car List One</h2>
  4. <ul>
  5. <li v-for="car in cars">
  6. <span>{{ car.name }}</span>
  7. <span>¥{{ car.price }}万元</span>
  8. </li>
  9. </ul>
  10. </div>
  11. </template>
  12.  
  13. <script>
  14. export default {
  15. props: ['cars'],
  16. data() {
  17. return {}
  18. }
  19. }
  20. </script>
  21.  
  22. <style scoped>
  23. h2 {
  24. color: dodgerblue;
  25. }
  26. </style>

CarListTwo.vue

  1. <template>
  2. <div id="car-list-two">
  3. <h2>Car List Two</h2>
  4. <ul>
  5. <li v-for="car in cars">
  6. <button>{{ car.name }}</button>
  7. <button>¥{{ car.price }}万元</button>
  8. </li>
  9. </ul>
  10. </div>
  11. </template>
  12.  
  13. <script>
  14. export default {
  15. props: ['cars'],
  16. data() {
  17. return {}
  18. }
  19. }
  20. </script>
  21.  
  22. <style scoped>
  23. h2 {
  24. color: limegreen;
  25. }
  26. </style>

3.4、State

state就是Vuex中的公共的状态, 我是将state看作是所有组件的data, 用于保存所有组件的公共数据。

此时我们就可以把App.vue中的两个组件共同使用的data抽离出来, 放到state中,代码如下:

  1. //main.js
  2. import Vue from 'vue'
  3. import App from './App.vue'
  4. import Vuex from 'vuex'
  5.  
  6. Vue.use( Vuex )
  7.  
  8. const store = new Vuex.Store({
  9. state:{
  10. cars: [
  11. {name: '奇瑞', price: 20},
  12. {name: '吉利', price: 40},
  13. {name: '长安', price: 60},
  14. {name: '比亚迪', price: 80}
  15. ]
  16. }
  17. })
  18.  
  19. new Vue({
  20. el: '#app',
  21. store,
  22. render: h => h(App)
  23. })
  24. 此时,CarListOne.vueCarListTwo.vue也需要做相应的更改
  25. //CarListOne.vue
  26. export default {
  27. data () {
  28. return {
  29. cars : this.$store.state.cars //获取store中state的数据
  30. }
  31. }
  32. }
  33. //CarListTwo.vue
  34. export default {
  35. data () {
  36. return {
  37. cars: this.$store.state.cars //获取store中state的数据
  38. }
  39. }
  40. }
  1. 此时的页面如下图所示, 可以看到, 将公共数据抽离出来后, 页面没有发生变化。

3.5、Getters

我将getters属性理解为所有组件的computed属性, 也就是计算属性。vuex的官方文档也是说到可以将getter理解为store的计算属性,getters的返回值会根据它的依赖被缓存起来,且只有当它的依赖值发生了改变才会被重新计算。

此时,我们可以在main.js中添加一个getters属性, 其中的saleCars对象将state中的价格减少一半(除以2)

  1. //main.js
  2. const store = new Vuex.Store({
  3. state:{
  4. cars: [
  5. {name: '奇瑞', price: 20},
  6. {name: '吉利', price: 40},
  7. {name: '长安', price: 60},
  8. {name: '比亚迪', price: 80}
  9. ]
  10. },
  11. getters:{ //添加getters
  12. saleCars: (state) => {
  13. let saleCars = state.cars.map( car => {
  14. return {
  15. name: car.name,
  16. price: car.price / 2
  17. }
  18. })
  19. return saleCars;
  20. }
  21. }
  22. })
  23. carListOne.vue中的cars的值更换为this.$store.getters.saleCars
  24. export default {
  25. data () {
  26. return {
  27. cars : this.$store.getters.saleCars
  28. }
  29. }
  30. }

现在的页面中,Car List One中的每项汽车的价格都减少了一半

  1. getters vue 中的 computed 类似 , 都是用来计算 state 然后生成新的数据 ( 状态 ) 的。
  2.  
  3. 还是前面的例子 , 假如我们需要一个与状态 show 刚好相反的状态 , 使用 vue 中的 computed 可以这样算出来 :
  4.  
  5. computed(){
  6. not_show(){
  7. return !this.$store.state.dialog.show;
  8. }
  9. }
  10. 那么 , 如果很多很多个组件中都需要用到这个与 show 刚好相反的状态 , 那么我们需要写很多很多个 not_show , 使用 getters 就可以解决这种问题 :
  11.  
  12. export default {
  13. state:{//state
  14. show:false
  15. },
  16. getters:{
  17. not_show(state){//这里的state对应着上面这个state
  18. return !state.show;
  19. }
  20. },
  21. mutations:{
  22. switch_dialog(state){//这里的state对应着上面这个state
  23. state.show = state.show?false:true;
  24. //你还可以在这里执行其他的操作改变state
  25. }
  26. },
  27. actions:{
  28. switch_dialog(context){//这里的context和我们使用的$store拥有相同的对象和方法
  29. context.commit('switch_dialog');
  30. //你还可以在这里触发其他的mutations方法
  31. },
  32. }
  33. }
  34. 我们在组件中使用 $store.state.dialog.show 来获得状态 show , 类似的 , 我们可以使用 $store.getters.not_show 来获得状态 not_show
  35.  
  36. 注意 : $store.getters.not_show 的值是不能直接修改的 , 需要对应的 state 发生变化才能修改。
  1. mapStatemapGettersmapActions
  2. 很多时候 , $store.state.dialog.show $store.dispatch('switch_dialog') 这种写法又长又臭 , 很不方便 , 我们没使用 vuex 的时候 , 获取一个状态只需要 this.show , 执行一个方法只需要 this.switch_dialog 就行了 , 使用 vuex 使写法变复杂了 ?
  3.  
  4. 使用 mapStatemapGettersmapActions 就不会这么复杂了。
  5.  
  6. mapState 为例 :
  7.  
  8. <template>
  9. <el-dialog :visible.sync="show"></el-dialog>
  10. </template>
  11.  
  12. <script>
  13. import {mapState} from 'vuex';
  14. export default {
  15. computed:{
  16.  
  17. //这里的三点叫做 : 扩展运算符
  18. ...mapState({
  19. show:state=>state.dialog.show
  20. }),
  21. }
  22. }
  23. </script>
  24. 相当于 :
  25.  
  26. <template>
  27. <el-dialog :visible.sync="show"></el-dialog>
  28. </template>
  29.  
  30. <script>
  31. import {mapState} from 'vuex';
  32. export default {
  33. computed:{
  34. show(){
  35. return this.$store.state.dialog.show;
  36. }
  37. }
  38. }
  39. </script>
  40. mapGettersmapActions mapState 类似 , mapGetters 一般也写在 computed , mapActions 一般写在 methods 中。

3.6、Mutations

我将mutaions理解为store中的methods, mutations对象中保存着更改数据的回调函数,该函数名官方规定叫type, 第一个参数是state, 第二参数是payload, 也就是自定义的参数.

下面,我们在main.js中添加mutations属性,其中minusPrice这个回调函数用于将汽车的价格减少payload这么多, 代码如下:

  1. //main.js
  2. const store = new Vuex.Store({
  3. state:{
  4. cars: [
  5. {name: '奇瑞', price: 20},
  6. {name: '吉利', price: 40},
  7. {name: '长安', price: 60},
  8. {name: '比亚迪', price: 80}
  9. ]
  10. },
  11. getters:{
  12. saleCars: (state) => {
  13. let saleCars = state.cars.map( car => {
  14. return {
  15. name: car.name,
  16. price: car.price / 2
  17. }
  18. })
  19. return saleCars;
  20. }
  21. },
  22. mutations:{ //添加mutations
  23. minusPrice (state, payload ) {
  24. let newPrice = state.cars.forEach( car => {
  25. car.price -= payload
  26. })
  27. }
  28. }
  29. })
  30. CarListTwo.vue中添加一个按钮,为其添加一个点击事件, 给点击事件触发minusPrice方法
  31. //CarListTwo.vue
  32. <template>
  33. <div id="car-list-two">
  34. <h2>Car List Two</h2>
  35. <ul>
  36. <li v-for="car in cars">
  37. <span class="name">{{ car.name }}</span>
  38. <span class="price">${{ car.price }}</span>
  39. </li>
  40. <button @click="minusPrice">减少价格</button> //添加按钮
  41. </ul>
  42. </div>
  43. </template>
  44. CarListTwo.vue中注册minusPrice方法, 在该方法中commitmutations中的minusPrice这个回调函数
  45. 注意:调用mutaions中回调函数, 只能使用store.commit(type, payload)
  46. //CarListTwo.vue
  47. export default {
  48. data () {
  49. return {
  50. cars: this.$store.state.cars
  51. }
  52. },
  53. methods: {
  54. minusPrice() {
  55. this.$store.commit('minusPrice', 2); //提交`minusPrice,payload为2
  56. }
  57. }
  58. }

添加按钮, 可以发现, Car List Two中的价格减少了2, 当然你可以自定义payload,以此自定义减少对应的价格.

mutations效果

(Car List One中的价格没有发生变化, 是因为getters将价格进行了缓存)

  1. 前面我们提到的对话框例子 , 我们对vuex 的依赖仅仅只有一个 $store.state.dialog.show 一个状态 , 但是如果我们要进行一个操作 , 需要依赖很多很多个状态 , 那管理起来又麻烦了 !
  2.  
  3. mutations 登场 , 问题迎刃而解 :
  4.  
  5. export default {
  6. state:{//state
  7. show:false
  8. },
  9. mutations:{
  10. switch_dialog(state){//这里的state对应着上面这个state
  11. state.show = state.show?false:true;
  12. //你还可以在这里执行其他的操作改变state
  13. }
  14. }
  15. }
  16. 使用 mutations , 原先我们的父组件可以改为 :
  17.  
  18. <template>
  19. <div id="app">
  20. <a href="javascript:;" @click="$store.commit('switch_dialog')">点击</a>
  21. <t-dialog></t-dialog>
  22. </div>
  23. </template>
  24.  
  25. <script>
  26. import dialog from './components/dialog.vue'
  27. export default {
  28. components:{
  29. "t-dialog":dialog
  30. }
  31. }
  32. </script>
  33. 使用 $store.commit('switch_dialog') 来触发 mutations 中的 switch_dialog 方法。
  34.  
  35. 这里需要注意的是:
  36.  
  37. mutations 中的方法是不分组件的 , 假如你在 dialog_stroe.js 文件中的定义了
  38. switch_dialog 方法 , 在其他文件中的一个 switch_dialog 方法 , 那么
  39. $store.commit('switch_dialog') 会执行所有的 switch_dialog 方法。
  40. mutations里的操作必须是同步的。
  41. 你一定好奇 , 如果在 mutations 里执行异步操作会发生什么事情 , 实际上并不会发生什么奇怪的事情 , 只是官方推荐 , 不要在 mutationss 里执行异步操作而已。

3.7、Actions

actions 类似于 mutations,不同在于:

  • actions提交的是mutations而不是直接变更状态

  • actions中可以包含异步操作, mutations中绝对不允许出现异步

  • actions中的回调函数的第一个参数是context, 是一个与store实例具有相同属性和方法的对象

  • 此时,我们在store中添加actions属性, 其中minusPriceAsync采用setTimeout来模拟异步操作,延迟2s执行 该方法用于异步改变我们刚才在mutaions中定义的minusPrice

  1. //main.js
  2. const store = new Vuex.Store({
  3. state:{
  4. cars: [
  5. {name: '奇瑞', price: 20},
  6. {name: '吉利', price: 40},
  7. {name: '长安', price: 60},
  8. {name: '比亚迪', price: 80}
  9. ]
  10. },
  11. getters:{
  12. saleCars: (state) => {
  13. let saleCars = state.cars.map( car => {
  14. return {
  15. name: car.name,
  16. price: car.price / 2
  17. }
  18. })
  19. return saleCars;
  20. }
  21. },
  22. mutations:{
  23. minusPrice (state, payload ) {
  24. let newPrice = state.cars.forEach( car => {
  25. car.price -= payload
  26. })
  27. }
  28. },
  29. actions:{ //添加actions
  30. minusPriceAsync( context, payload ) {
  31. setTimeout( () => {
  32. context.commit( 'minusPrice', payload ); //context提交
  33. }, 2000)
  34. }
  35. }
  36. })
  37. CarListTwo.vue中添加一个按钮,为其添加一个点击事件, 给点击事件触发minusPriceAsync方法
  38. <template>
  39. <div id="car-list-two">
  40. <h2>Car List Two</h2>
  41. <ul>
  42. <li v-for="car in cars">
  43. <span class="name">{{ car.name }}</span>
  44. <span class="price">${{ car.price }}</span>
  45. </li>
  46. <button @click="minusPrice">减少价格</button>
  47. <button @click="minusPriceAsync">异步减少价格</button> //添加按钮
  48. </ul>
  49. </div>
  50. </template>
  51. CarListTwo.vue中注册minusPriceAsync方法, 在该方法中dispatchactions中的minusPriceAsync这个回调函数
  52. export default {
  53. data () {
  54. return {
  55. cars: this.$store.state.cars
  56. }
  57. },
  58. methods: {
  59. minusPrice() {
  60. this.$store.commit('minusPrice', 2);
  61. },
  62. minusPriceAsync() {
  63. this.$store.dispatch('minusPriceAsync', 5); //分发actions中的minusPriceAsync这个异步函数
  64. }
  65. }
  66. }
  • 添加按钮, 可以发现, Car List Two中的价格延迟2s后减少了5

    actions效果
     
     
     
    1. 多个 state 的操作 , 使用 mutations 会来触发会比较好维护 , 那么需要执行多个 mutations 就需要用 action 了:
    2.  
    3. export default {
    4. state:{//state
    5. show:false
    6. },
    7. mutations:{
    8. switch_dialog(state){//这里的state对应着上面这个state
    9. state.show = state.show?false:true;
    10. //你还可以在这里执行其他的操作改变state
    11. }
    12. },
    13. actions:{
    14. switch_dialog(context){//这里的context和我们使用的$store拥有相同的对象和方法
    15. context.commit('switch_dialog');
    16. //你还可以在这里触发其他的mutations方法
    17. },
    18. }
    19. }
    20. 那么 , 在之前的父组件中 , 我们需要做修改 , 来触发 action 里的 switch_dialog 方法:
    21.  
    22. <template>
    23. <div id="app">
    24. <a href="javascript:;" @click="$store.dispatch('switch_dialog')">点击</a>
    25. <t-dialog></t-dialog>
    26. </div>
    27. </template>
    28.  
    29. <script>
    30. import dialog from './components/dialog.vue'
    31. export default {
    32. components:{
    33. "t-dialog":dialog
    34. }
    35. }
    36. </script>
    37. 使用 $store.dispatch('switch_dialog') 来触发 action 中的 switch_dialog 方法。
    38.  
    39. 官方推荐 , 将异步操作放在 action 中。
     

3.8、Modules

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

  1. const moduleA = {
  2. state: { ... },
  3. mutations: { ... },
  4. actions: { ... },
  5. getters: { ... }
  6. }
  7.  
  8. const moduleB = {
  9. state: { ... },
  10. mutations: { ... },
  11. actions: { ... }
  12. }
  13.  
  14. const store = new Vuex.Store({
  15. modules: {
  16. a: moduleA,
  17. b: moduleB
  18. }
  19. })
  20.  
  21. store.state.a // -> moduleA 的状态
  22. store.state.b // -> moduleB 的状态
  1. 前面为了方便 , 我们把 store 对象写在了 main.js 里面 , 但实际上为了便于日后的维护 , 我们分开写更好 , 我们在 src 目录下 , 新建一个 store 文件夹 , 然后在里面新建一个 index.js :
  2.  
  3. import Vue from 'vue'
  4. import vuex from 'vuex'
  5. Vue.use(vuex);
  6.  
  7. export default new vuex.Store({
  8. state:{
  9. show:false
  10. }
  11. })
  12. 那么相应的 , main.js 里的代码应该改成 :
  13.  
  14. //vuex
  15. import store from './store'
  16.  
  17. new Vue({
  18. el: '#app',
  19. router,
  20. store,//使用store
  21. template: '<App/>',
  22. components: { App }
  23. })
  24. 这样就把 store 分离出去了 , 那么还有一个问题是 : 这里 $store.state.show 无论哪个组件都可以使用 , 那组件多了之后 , 状态也多了 , 这么多状态都堆在 store 文件夹下的 index.js 不好维护怎么办 ?
  25.  
  26. 我们可以使用 vuex modules , store 文件夹下的 index.js 改成 :
  27.  
  28. import Vue from 'vue'
  29. import vuex from 'vuex'
  30. Vue.use(vuex);
  31.  
  32. import dialog_store from '../components/dialog_store.js';//引入某个store对象
  33.  
  34. export default new vuex.Store({
  35. modules: {
  36. dialog: dialog_store
  37. }
  38. })
  39. 这里我们引用了一个 dialog_store.js , 在这个 js 文件里我们就可以单独写 dialog 组件的状态了 :
  40.  
  41. export default {
  42. state:{
  43. show:false
  44. }
  45. }
  46. 做出这样的修改之后 , 我们将之前我们使用的 $store.state.show 统统改为 $store.state.dialog.show 即可。
  47.  
  48. 如果还有其他的组件需要使用 vuex , 就新建一个对应的状态文件 , 然后将他们加入 store 文件夹下的 index.js 文件中的 modules 中。
  49.  
  50. modules: {
  51. dialog: dialog_store,
  52. other: other,//其他组件
  53. }

改进的计算器

  1. egstore.js
  2.  
  3. import Vue from 'vue';
  4. import Vuex from 'vuex'; //引入 vuex
  5. import store from './store' //注册store
  6.  
  7. Vue.use(Vuex); //使用 vuex
  8.  
  9. export default new Vuex.Store({
  10. state: {
  11. // 初始化状态
  12. count: 0,
  13. someLists:[]
  14. },
  15. mutations: {
  16. // 处理状态
  17. increment(state, payload) {
  18. state.count += payload.step || 1;
  19. }
  20. },
  21. actions: {
  22. // 提交改变后的状态
  23. increment(context, param) {
  24. context.state.count += param.step;
  25. context.commit('increment', context.state.count)//提交改变后的state.count值
  26. },
  27. incrementStep({state, commit, rootState}) {
  28. if (rootState.count < 100) {
  29. store.dispatch('increment', {//调用increment()方法
  30. step: 10
  31. })
  32. }
  33. }
  34. },
  35. getters: {
  36. //处理列表项
  37. someLists: state =>param=> {
  38. return state.someLists.filter(() => param.done)
  39. }
  40. }
  41. })
  42. 使用时,eg
  43.  
  44. main.js
  45.  
  46. import Vue from 'vue'
  47. import App from './App.vue'
  48. import router from './router'
  49. import store from './store' //引入状态管理 store
  50.  
  51. Vue.config.productionTip = false
  52.  
  53. new Vue({
  54. router,
  55. store,//注册store(这可以把 store 的实例注入所有的子组件)
  56. render: h => h(App)
  57. }).$mount('#app')
  58. views/home.vue
  59.  
  60. <template>
  61. <div class="home">
  62. <!--在前端HTML页面中使用 count-->
  63. <HelloWorld :msg="count"/>
  64. <!--表单处理 双向绑定 count-->
  65. <input :value="count" @input="incrementStep">
  66. </div>
  67. </template>
  68.  
  69. <script>
  70. import HelloWorld from '@/components/HelloWorld.vue'
  71. import {mapActions, mapState,mapGetters} from 'vuex' //注册 action 和 state
  72.  
  73. export default {
  74. name: 'home',
  75. computed: {
  76. //在这里映射 store.state.count,使用方法和 computed 里的其他属性一样
  77. ...mapState([
  78. 'count'
  79. ]),
  80. count () {
  81. return store.state.count
  82. }
  83. },
  84. created() {
  85. this.incrementStep();
  86. },
  87. methods: {
  88. //在这里引入 action 里的方法,使用方法和 methods 里的其他方法一样
  89. ...mapActions([
  90. 'incrementStep'
  91. ]),
  92. // 使用对象展开运算符将 getter 混入 computed 对象中
  93. ...mapGetters([
  94. 'someLists'
  95. // ...
  96. ])
  97. },
  98. components: {
  99. HelloWorld
  100. }
  101. }
  102. </script>

前端MVC Vue2学习总结(九)——Vuex状态管理插件的更多相关文章

  1. 前端MVC Vue2学习总结(八)——Vue Router路由、Vuex状态管理、Element-UI

    一.Vue Router路由 二.Vuex状态管理 三.Element-UI Element-UI是饿了么前端团队推出的一款基于Vue.js 2.0 的桌面端UI框架,手机端有对应框架是 Mint U ...

  2. 前端技术之:如何在vuex状态管理action异步调用结束后执行UI中的方法

    一.问题的起源 最近在做vue.js项目时,遇到了vuex状态管理action与vue.js方法互相通信.互操作的问题.场景如下图所示: 二.第一种解决方法 例如,我们在页面初始化的时候,需要从服务端 ...

  3. 前端MVC Vue2学习总结(一)——MVC与vue2概要、模板、数据绑定与综合示例

    一.前端MVC概要 1.1.库与框架的区别 框架是一个软件的半成品,在全局范围内给了大的约束.库是工具,在单点上给我们提供功能.框架是依赖库的.Vue是框架而jQuery则是库. 1.2.AMD与CM ...

  4. 前端MVC Vue2学习总结(二)——Vue的实例、生命周期与Vue脚手架(vue-cli)

    一.Vue的实例 1.1.创建一个 Vue 的实例 每个 Vue 应用都是通过 Vue 函数创建一个新的 Vue 实例开始的: var vm = new Vue({ // 选项 }) 虽然没有完全遵循 ...

  5. 前端MVC Vue2学习总结(七)——ES6与Module模块化、Vue-cli脚手架搭建、开发、发布项目与综合示例

    使用vue-cli可以规范项目,提高开发效率,但是使用vue-cli时需要一些ECMAScript6的知识,特别是ES6中的模块管理内容,本章先介绍ES6中的基础与模块化的内容再使用vue-cli开发 ...

  6. 前端MVC Vue2学习总结(五)——表单输入绑定、组件

    一.表单输入绑定 1.1.基础用法 你可以用 v-model 指令在表单控件元素上创建双向数据绑定.它会根据控件类型自动选取正确的方法来更新元素.尽管有些神奇,但 v-model 本质上不过是语法糖, ...

  7. 前端MVC Vue2学习总结(六)——axios与跨域HTTP请求、Lodash工具库

    一.axios Vue更新到2.0之后宣告不再对vue-resource更新,推荐使用axios,axios是一个用于客户端与服务器通信的组件,axios 是一个基于Promise 用于浏览器和 no ...

  8. 前端MVC Vue2学习总结(三)——模板语法、过滤器、计算属性、观察者、Class 与 Style 绑定

    Vue.js 使用了基于 HTML 的模版语法,允许开发者声明式地将 DOM 绑定至底层 Vue 实例的数据.所有 Vue.js 的模板都是合法的 HTML ,所以能被遵循规范的浏览器和 HTML 解 ...

  9. 前端MVC Vue2学习总结(四)——条件渲染、列表渲染、事件处理器

    一.条件渲染 1.1.v-if 在字符串模板中,如 Handlebars ,我们得像这样写一个条件块: <!-- Handlebars 模板 --> {{#if ok}} <h1&g ...

随机推荐

  1. LeetCode 买卖股票的最佳时机 II

    给定一个数组,它的第 i 个元素是一支给定股票第 i 天的价格. 设计一个算法来计算你所能获取的最大利润.你可以尽可能地完成更多的交易(多次买卖一支股票). 注意:你不能同时参与多笔交易(你必须在再次 ...

  2. codevs 数字三角形集结

    添在前面的一句话:初学DP,若有错误,请指出,不能误人子弟,欢迎大家提出意见.水平不高,博客写的比较粗糙,代码也挺丑,请见谅. 最原始的数字三角形: 1220 数字三角形  时间限制: 1 s  空间 ...

  3. 【php】关于尾部去除和分号问题

    One thing to remember is, if you decide to omit the closing PHP tag, then the last line of the file ...

  4. Ubuntu 15.04 Qt5 链接 mysql数据库

    序 最近在Ubuntu15.04下做一个Linux-服务器-客户端通信项目,用到MySQL数据库.开始的时候,在数据库链接时遇到障碍,查找资料解决. 特此记录,分享于此. 环境配置 系统:Ubuntu ...

  5. 安装elk,日志采集系统

    #elasticsearch安装 wget https://artifacts.elastic.co/downloads/elasticsearch/elasticsearch-7.0.0-linux ...

  6. luogu3698 [CQOI2017]小Q的棋盘

    最长链是根节点到深度最深的结点的路径. 显然,要么直接走最长链,要么兜兜转转几个圈圈再走最长链,而最长链以外的结点因为要"兜圈",所以要经过两次. #include <ios ...

  7. Java总结输入流输出流

    学习Java的同学注意了!!!  学习过程中遇到什么问题或者想获取学习资源的话,欢迎加入Java学习交流群:618528494  我们一起学Java! 1.什么是IO Java中I/O操作主要是指使用 ...

  8. 四、harbor实践之初识harbor

    1 什么是Harbor harbor是VMware公司开源的企业级Registry项目,其的目标是帮助用户迅速搭建一个企业级的Docker registry 服务. 2 什么是Registry Reg ...

  9. appium+python自动化-adb shell模拟点击事件(input tap)

    前言 appium有时候定位一个元素很难定位到,或者说明明定位到这个元素了,却无法点击,这个时候该怎么办呢? 求助大神是没用的,点击不了就是点击不了,appium不是万能的,这个时候应该转换思路,换其 ...

  10. mysql replication常见错误整理

    这篇文章旨在记录MySQL Replication的常见错误,包括自己工作中遇到的与网友在工作中遇到的,方面自己及别人以后进行查找.每个案例都是通过Last_IO_Errno/Last_IO_Erro ...