小菜鸡的成长之路(vuex-vue组件通讯、事件车、vuex)

210 阅读6分钟

课程目标

  • 组件通信方式回顾
  • Vuex 核心概念和基本使用回顾
  • 购物车案例
  • 模拟实现 Vuex

组件内的状态管理流程

Vue 最核心的两个功能:数据驱动和组件化。

组件化开发给我们带来了:

  • 更快的开发效率
  • 更好的可维护性

每个组件都有自己的状态、视图和行为等组成部分。

new Vue({
 // state
 data () {
  return {
   count: 0
 }
},
 // view
 template: `
  <div>{{ count }}</div>
 `,
 // actions
 methods: {
  increment () {
   this.count++
 }
}
})

状态管理包含以下几部分:

  • state,驱动应用的数据源;

  • view,以声明方式将 state 映射到视图;

  • actions,响应在 view 上的用户输入导致的状态变化。

image.png

组件间通信方式回顾

大多数场景下的组件都并不是独立存在的,而是相互协作共同构成了一个复杂的业务功能。在 Vue 中为

不同的组件关系提供了不同的通信规则

image.png

父传子:Props Down

<blog-post title="My journey with Vue"></blog-post>
// 子
Vue.component('blog-post', {
 props: ['title'],
 template: '<h3>{{ title }}</h3>'
})

子传父:Event Up

在子组件中使用 $emit 发布一个自定义事件:

<button v-on:click="$emit('enlargeText', 0.1)">
Enlarge text
</button>

在使用这个组件的时候,使用 v-on 监听这个自定义事件

使用事件抛出一个值

<blog-post v-on:enlargeText="hFontSize += $event"></blog-post>

非父子组件:Event Bus

我们可以使用一个非常简单的 Event Bus 来解决这个问题:

eventbus.js :

export default new Vue()

然后在需要通信的两端:

使用 $on 订阅:

// 没有参数
bus.$on('自定义事件名称', () => {
 // 执行操作
})
// 有参数
bus.$on('自定义事件名称', data => {
 // 执行操作
})

使用 $emit 发布:

// 没有自定义传参
bus.$emit('自定义事件名称');
// 有自定义传参
bus.$emit('自定义事件名称', 数据);

父直接访问子组件:通过 ref 获取子组件

ref 有两个作用:

  • 如果你把它作用到普通 HTML 标签上,则获取到的是 DOM
  • 如果你把它作用到组件标签上,则获取到的是组件实例

创建 base-input 组件

<template>
 <input ref="input">
</template>
<script>
 export default {
  methods: {
   // 用来从父级组件聚焦输入框
   focus: function () {
    this.$refs.input.focus()
  }
 }
}
</script>

在使用子组件的时候,添加 ref 属性

<base-input ref="usernameInput"></base-input>

然后在父组件等渲染完毕后使用 $refs 访问:

mounted () {
 this.$refs.usernameInput.focus()
}

$refs 只会在组件渲染完成之后生效,并且它们不是响应式的。这仅作为一个用于直接操作子组

件的“逃生舱”——你应该避免在模板或计算属性中访问 $refs 。

简易的状态管理方案

如果多个组件之间要共享状态(数据),使用上面的方式虽然可以实现,但是比较麻烦,而且多个组件之

间互相传值很难跟踪数据的变化,如果出现问题很难定位问题。

当遇到多个组件需要共享状态的时候,典型的场景:购物车。我们如果使用上述的方案都不合适,我们

会遇到以下的问题

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

对于问题一,传参的方法对于多层嵌套的组件将会非常繁琐,并且对于兄弟组件间的状态传递无能为

力。

对于问题二,我们经常会采用父子组件直接引用或者通过事件来变更和同步状态的多份拷贝。以上的这

些模式非常脆弱,通常会导致无法维护的代码。

因此,我们为什么不把组件的共享状态抽取出来,以一个全局单例模式管理呢?在这种模式下,我们的

组件树构成了一个巨大的“视图”,不管在树的哪个位置,任何组件都能获取状态或者触发行为!

我们可以把多个组件的状态,或者整个程序的状态放到一个集中的位置存储,并且可以检测到数据的更

改。你可能已经想到了 Vuex。

这里我们先以一种简单的方式来实现

  • 首先创建一个共享的仓库 store 对象

    export default { debug: true, state: { user: { name: 'xiaomao', age: 18, sex: '男' } }, setUserNameAction (name) { if (this.debug) { console.log('setUserNameAction triggered:', name) } this.state.user.name = name } }

  • 把共享的仓库 store 对象,存储到需要共享状态的组件的 data 中

    import store from './store' export default { methods: { // 点击按钮的时候通过 action 修改状态 change () { store.setUserNameAction('componentB') } }, data () { return { privateState: {}, sharedState: store.state } } }

接着我们继续延伸约定,组件不允许直接变更属于 store 对象的 state,而应执行 action 来分发

(dispatch) 事件通知 store 去改变,这样最终的样子跟 Vuex 的结构就类似了。这样约定的好处是,我

们能够记录所有 store 中发生的 state 变更,同时实现能做到记录变更、保存状态快照、历史回滚/时光

旅行的先进的调试工具.

Vuex 回顾

什么是vuex

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

什么情况下使用vuex

  • 非必要情况不使用vuex
  • 多个视图依赖同一种状态
  • 来自不同视图的行为需要变更同一状态

vuex的核心概念

image.png

  • store:是一个容器,包含着应用中的大部分状态,不能直接改变store中的状态,要通过mutation的方式改变状态
  • state:Vuex 使用单一状态树,用一个对象就包含了全部的应用层级状态
  • getter:就是 store 中的计算属性,使用 mapGetter 简化视图中的使用
  • mutation:更改 Vuex 的 store 中的状态的唯一方法是提交 mutation。Vuex 中的 mutation 非常类似于事件:每

个 mutation 都有一个字符串的 事件类型 (type) 和 一个 回调函数 (handler)。

  • action:Action 类似于 mutation,不同在于:

  • Action 提交的是 mutation,而不是直接变更状态。

  • Action 可以包含任意异步操作。

  • module:由于使用单一状态树,应用的所有状态会集中到一个比较大的对象。当应用变得非常复杂时,store 对

象就有可能变得相当臃肿。

为了解决以上问题,Vuex 允许我们将 store 分割成模块(module)。每个模块拥有自己的 state、

mutation、action、getter、甚至是嵌套子模块。

1. Vuex基本结构

定义store:store/index.js

import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
export default new Vuex.Store({
  state: {},
  mutations: {},
  actions: {},
  modules: {}
})

注入store:

import store from './store'
new Vue({
  router,
  store,
  render: h => h(App)
}).$mount('#app')

2. State

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: {
  }
})

App.vue

<template>
  <div id="app">
    <h1>Vuex - Demo </h1>
    <!-- <p>count: {{ $store.state.count }}</p> -->
    <!-- <p>msg: {{ $store.state.msg }}</p> -->
    <p>count: {{ count }}</p>
    <p>msg: {{ msg }}</p>
  </div>
</template>
<script>
import { mapState } from 'vuex'
export default {
  name: 'App',
  computed: {
    // count: state => state.count,
    ...mapState(['count', 'msg'])
  }
}
</script>

$store.state.xxxmapState展开两种写法都可以。不过使用mapState展开成计算属性时,如果原本就有这个属性名,那么mapState展开的属性就不起作用,可以通过给属性重命名的方式更改计算属性的名称:

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

...mapState({ num: 'count', message: 'msg' })

3. Getter

Vuex中的getter相当于VUE中的计算属性。

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: {
  }
})

App.vue

<template>
  <div id="app">
    <h1>Vuex - Demo </h1>
    <!-- <p>count: {{ $store.state.count }}</p> -->
    <!-- <p>msg: {{ $store.state.msg }}</p> -->
    <!-- <p>count: {{ count }}</p> -->
    <!-- <p>msg: {{ msg }}</p> -->
    <p>count: {{ num }}</p>
    <p>msg: {{ message }}</p>
    <h2>Getter</h2>
    <!-- <p>reverseMsg: {{ $store.getters.reverseMsg }}</p> -->
    <p>reverseMsg: {{ reverseMsg }}</p>
  </div>
</template>
<script>
import { mapState, mapGetters } from 'vuex'
export default {
  name: 'App',
  computed: {
    // count: state => state.count,
    // ...mapState(['count', 'msg']),
    ...mapState({ num: 'count', message: 'msg' }),
    ...mapGetters(['reverseMsg'])
  }
}
</script>

使用mapGetters将VUE中的getters导入到Vue的计算属性中,用法和mapState类似。

4. Mutation

Mutation中修改state,只能支持同步操作。

store/index.js

mutations: {
  increate (state, payload) {
    state.count += payload
  }
},

在模板中通过$store.emit()来提交mutation。

App.vue

<button @click="$store.commit('increate', 3)">Mutation</button>

而mutation的本质是方法,所以可以通过mapMutations将mutation中的方法展开到Vue的methods中

App.vue

<template>
  <div id="app">
    <h1>Vuex - Demo </h1>
    <!-- <p>count: {{ $store.state.count }}</p> -->
    <!-- <p>msg: {{ $store.state.msg }}</p> -->
    <!-- <p>count: {{ count }}</p> -->
    <!-- <p>msg: {{ msg }}</p> -->
    <p>count: {{ num }}</p>
    <p>msg: {{ message }}</p>
    <h2>Getter</h2>
    <!-- <p>reverseMsg: {{ $store.getters.reverseMsg }}</p> -->
    <p>reverseMsg: {{ reverseMsg }}</p>
    <h2>Mutation</h2>
    <!-- <button @click="$store.commit('increate', 2)">Mutation</button> -->
    <button @click="increate(3)">Mutation</button>
  </div>
</template>
<script>
import { mapState, mapGetters, mapMutations } from 'vuex'
export default {
  name: 'App',
  computed: {
    // count: state => state.count,
    // ...mapState(['count', 'msg']),
    ...mapState({ num: 'count', message: 'msg' }),
    ...mapGetters(['reverseMsg'])
  },
  methods: {
    ...mapMutations(['increate'])
  }
}
</script>

打开Vue调试工具,可以看到vuex中的mutation变化,每个mutation上面的三个按钮,分别是提交本次mutation、恢复到本次的mutation、时光旅行。

image

5. Action

Action中可以进行异步操作,不过如果需要修改state,得提交Mutation。

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)
      }, 2000)
    }
  },
  modules: {
  }
})

App.vue

mapActions用法同mapMutations

<template>
  <div id="app">
    <h1>Vuex - Demo </h1>
    <!-- <p>count: {{ $store.state.count }}</p> -->
    <!-- <p>msg: {{ $store.state.msg }}</p> -->
    <!-- <p>count: {{ count }}</p> -->
    <!-- <p>msg: {{ msg }}</p> -->
    <p>count: {{ num }}</p>
    <p>msg: {{ message }}</p>
    <h2>Getter</h2>
    <!-- <p>reverseMsg: {{ $store.getters.reverseMsg }}</p> -->
    <p>reverseMsg: {{ reverseMsg }}</p>
    <h2>Mutation</h2>
    <!-- <button @click="$store.commit('increate', 2)">Mutation</button> -->
    <button @click="increate(3)">Mutation</button>
    <h2>Action</h2>
    <!-- <button @click="$store.dispatch('increateAsync', 5)">Mutation</button> -->
    <button @click="increateAsync(6)">Mutation</button>
  </div>
</template>
<script>
import { mapState, mapGetters, mapMutations, mapActions } from 'vuex'
export default {
  name: 'App',
  computed: {
    // count: state => state.count,
    // ...mapState(['count', 'msg']),
    ...mapState({ num: 'count', message: 'msg' }),
    ...mapGetters(['reverseMsg'])
  },
  methods: {
    ...mapMutations(['increate']),
    ...mapActions(['increateAsync'])
  }
}
</script>

6. Module

Module可以让单一状态树拆分成多个模块,每个模块可以拥有state、mutation、action、getter,甚至嵌套子模块。

在使用模块里的数据时,可以通过$store.模块名.state状态属性名的方式访问

在使用模块里的方法时,可以通过$store.commit('mutation方法名')的方式提交mutation

当想要有更强的封装性时,可以开启命名空间,在导出的模块对象里增加一个namespaced属性为true,然后就可以在Vue中使用 mapState('模块名', ['state状态属性名'])的方式获取到属性名称,使用mapMutations('模块名', ['mutation方法名'])的方式获取到方法名。

./store/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
}

./store/modules/cart.js

const state = {}
const getters = {}
const mutations = {}
const actions = {}
export default {
  namespaced: true,
  state,
  getters,
  mutations,
  actions
}

./store/index.js

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({
  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
  }
})

App.vue

<template>
  <div id="app">
    <h1>Vuex - Demo </h1>
    <!-- <p>count: {{ $store.state.count }}</p> -->
    <!-- <p>msg: {{ $store.state.msg }}</p> -->
    <!-- <p>count: {{ count }}</p> -->
    <!-- <p>msg: {{ msg }}</p> -->
    <p>count: {{ num }}</p>
    <p>msg: {{ message }}</p>
    <h2>Getter</h2>
    <!-- <p>reverseMsg: {{ $store.getters.reverseMsg }}</p> -->
    <p>reverseMsg: {{ reverseMsg }}</p>
    <h2>Mutation</h2>
    <!-- <button @click="$store.commit('increate', 2)">Mutation</button> -->
    <button @click="increate(3)">Mutation</button>
    <h2>Action</h2>
    <!-- <button @click="$store.dispatch('increateAsync', 5)">Mutation</button> -->
    <button @click="increateAsync(6)">Mutation</button>
    <h2>Modules</h2>
    <!-- <p>products: {{ $store.state.products.products }}</p> -->
    <!-- <button @click="$store.commit('setProducts', [])">Mutation</button> -->
    <p>products: {{ products }}</p>
    <button @click="setProducts([])">Mutation</button>
  </div>
</template>
<script>
import { mapState, mapGetters, mapMutations, mapActions } from 'vuex'
export default {
  name: 'App',
  computed: {
    // count: state => state.count,
    // ...mapState(['count', 'msg']),
    ...mapState({ num: 'count', message: 'msg' }),
    ...mapGetters(['reverseMsg']),
    ...mapState('products', ['products'])
  },
  methods: {
    ...mapMutations(['increate']),
    ...mapActions(['increateAsync']),
    ...mapMutations('products', ['setProducts'])
  }
}
</script>

7.严格模式

Vuex中的状态的更新要通过提交mutation来修改,但其实在组件中还可以通过$store.state.msg进行修改,从语法从面来说这是没有问题的,但是这破坏了Vuex的约定,如果在组件中直接修改state,devtools无法跟踪到这次状态的修改。

开启严格模式之后,如果在组件中直接修改state会抛出错误,但数据仍被成功修改。

如何开启:在store中增加一个属性strict为true

store/index.js

export default new Vuex.Store({
  strict: process.env.NODE_ENV !== 'production',
  state: {
    count: 0,
    msg: 'Hello Vuex'
  },
  // ...
})

App.vue

<h2>strict</h2>
<button @click="$store.state.msg = 'lagou'">strict</button>

image

注意:不要在生产模式下开启严格模式,严格模式会深度检查状态树,检查不合规的状态改变,会影响性能。

我们可以在开发模式下开启严格模式,在生产模式中关闭严格模式:

strict: process.env.NODE_ENV !== 'production',

购物车案例

1. 模板

地址:github.com/goddlts/vue…

用到了ElementUI、Vuex、Vue-Router

项目根目录下的server.js文件是一个node服务,为了模拟项目接口。

页面组件和路由已经完成了,我们需要使用Vuex完成数据的交互。

2.组件设计

2.1商品列表组件

  • 展示商品列表
  • 添加购物车

2.2我的购物车组件

  • 删除商品
  • 购物列表
  • 数量
  • 总计

2.1商品列表组件

  • 全选

  • 单选

  • 删除

  • 增加数量

  • 小计、总计

模拟实现Vuex

Myvuex/index.js

// vue 实例
let _Vue = null
// 基础类
class Store {
  constructor (options) {
    const {
      state = {},
      getters = {},
      mutations = {},
      actions = {}
    } = options
    // 注册成响应对象
    this.state = _Vue.observable(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
  }

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

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

// install
function install (Vue) {
  _Vue = Vue
  // 所有使用vue.use的插件都需要在内部混入install
  _Vue.mixin({
    beforeCreate () {
      if (this.$options.store) {
        _Vue.prototype.$store = this.$options.store
      }
    }
  })
}

// 暴露
export default {
  Store,
  install
}

将store/index.js中的vuex的导入替换成myvuex

import Vuex from '../myvuex'