大前端学习--Vuex状态管理

Vuex状态管理

文章内容输出来源:拉勾教育大前端高薪训练营

一、组件内的状态管理流程

1. 状态管理

  • state:驱动应用的数据源

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

  • actions:相应在view上的用户输入导致的状态变化

    大前端学习--Vuex状态管理_第1张图片

二、组件间通信方式

1. 父组件给子组件传值

  • 子组件通过props接受数据

    <template>
    	<div>
        <h2>{{title}}h2>
      div>
    template>
    
    <script>
    export default {
      // props: ['title'],
      props: {
        title: String
      }
    }
    script>
    
  • 父组件中给子组件通过响应属性传值

    <template>
    	<div>
        <child title="My journey with Vue">child>
      div>
    template>
    
    <script>
    import child from './01-Child'
      export default {
        components: {
          child
        }
      }
    script>
    

2. 子组件给父组件传值

  • 通过自定义事件子组件给父组件传值

    <template>
    	<div>
        <h1 :style="{ fontSize: fontSize + 'em' }">Props Down Childh1>
        <button @click="handler">文字增大button>
      div>
    template>
    
    <script>
    export default {
      props: {
        fontSize: Number
      },
      methods: {
        handler () {
          this.$emit('enlargeText', 01)
    		}
      }
    }
    script>
    
  • 父组件中注册子组件内部触发的事件

    <template>
    	<div>
        <h1 :style="{ fontSize: hFontSize + 'em' }">Event Up Parenth1>
        
        这里的文字不需要变化
        
        <child :fontSize="hFontSize" v-on:enlargeText="enlargeText">child>
        <child :fontSize="hFontSize" v-on:enlargeText="enlargeText">child>
    		<child :fontSize="hFontSize" v-on:enlargeText="hFontSize + $event">child>
      div>
    template>
    
    <script>
    import child from './02-child'
    export default {
      components: {
        child
      },
      data () {
        return {
          hFontSize: 1
        }
      },
      methods: {
        enlargeText (size) {
          this.hFontSize += size
        }
      }
    }
    script>
    

3. 不相关组件之间传值

  • 通过事件中心eventbus触发和注册事件

    import Vue from 'vue'
    export default new Vue()
    
  • 触发eventbus中的事件

    <template>
    	<div>
        <h1>Event Bus sibling01h1>
        <div class="number" @click="sub">-div>
        
        <div class="number" @click="add">+div>
      div>
    template>
    
    <script>
    import bus from './eventbus'
      
    export default {
      props: {
        num: Number
      },
      created () {
        this.value = this.num
      },
      data () {
        return {
          value: -1
        }
      },
      methods: {
    		sub () {
          if (this.value > 1) {
            this.value--
            bus.$emit('numchange', this.value)
          }
        },
        add () {
          this.value++
          bus.$emit('numchange', this.value)
        }
      }
    }
    script>
    
  • 注册事件

    <template>
    	<div>
        <h1>Event Bus sibling02h1>
        
        <div>{{ msg }}div>
      div>
    template>
    
    <script>
    import bus from './eventbus'
    export default {
      data () {
        return {
          msg: ''
        }
      },
      created () {
    		bus.$on('numchange', (value) => {
          this.msg = `您选择了${value}件商品`
        })
      }
    }
    script>
    

4. 其他常见方式(不推荐使用)

  • $root
  • $parent
  • $children
  • $ref

ref两个作用:

  1. 在普通HTML标签上使用ref,获取到的是DOM

  2. 在组件标签上使用ref,获取到的是组件实例

子组件中定义ref

<template>
	<div>
    <h1>
      ref Child
  	h1>
    <input ref="input" type="text" v-model="value">
  div>
template>

<script>
export default {
  data () {
    return {
      value: ''
    }
  },
  methods: {
    focus () {
      this.$refs.input.focus()
    }
  }
}
script>

父组件获取子组件的ref

<template>
	<div>
    <h1>
      ref Parent
  	h1>
    
    <child ref="c">child>
  div>
template>

<script>
import child from './04-child'
export default {
  components: {
    child
  },
  mounted () {
    this.$refs.c.focus()
    this.$refs.c.value = 'hello input'
  }
}
script>

ref这种方式不到万不得已不要使用,会导致数据的混乱。

三、简易的状态管理方案

1. 父子组件传值的问题

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

2. 集中式的状态管理

store.js

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
  }
}

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>

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>

四、Vuex回顾

1. 什么是Vuex

  • Vuex是专门为Vue.js设计的状态管理库
  • Vuex采用集中式的方式存储需要共享的状态
  • Vuex的作用是进行状态管理,解决复杂组件通信,数据共享
  • Vuex集成到了devtools中,提供了time-travel时光旅行历史回滚功能

2. 什么情况下使用Vuex

  • 非必要的情况不要使用Vuex
  • 大型的单页应用程序
    • 多个视图依赖于同一状态
    • 来自不同视图的行为需要变更同一状态

3. Vuex核心概念

  • Store: 是一个容器,包含着应用中的大部分状态,不能直接改变store中的状态,要通过mutation的方式改变状态。
  • State:是状态,保存在Store中,因为Store是唯一的,所以State也是唯一的,也称为单一状态树。这里的状态是响应式的。
  • Getter:是Vuex中的计算属性,方便从一个属性派生出其他的值。它内部会对计算的属性进行缓存,只有当依赖改变的时候,才会重新进行计算。
  • Mutation:状态的变换必须要通过提交Mutation来完成。
  • Action:和MuTation类似,不同的是Action可以进行异步的操作,内部改变状态的时候,都需要提交Mutation。
  • Module:当Store太过臃肿时,可以将Store分成多个模块,每个模块里有State、Mutation、Action、Getter,甚至是子模块。

五、Vuex使用

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

大前端学习--Vuex状态管理_第2张图片

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: {{ 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: {{ num }}p>
    <p>msg: {{ message }}p>
    <h2>Getterh2>
    
    <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)">Mutationbutton>

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

App.vue

<template>
  <div id="app">
    <h1>Vuex - Demo h1>
    
    
    
    
    <p>count: {{ num }}p>
    <p>msg: {{ message }}p>
    <h2>Getterh2>
    
    <p>reverseMsg: {{ reverseMsg }}p>

    <h2>Mutationh2>
    
    <button @click="increate(3)">Mutationbutton>
  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、时光旅行。
大前端学习--Vuex状态管理_第3张图片

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: {{ num }}p>
    <p>msg: {{ message }}p>
    <h2>Getterh2>
    
    <p>reverseMsg: {{ reverseMsg }}p>

    <h2>Mutationh2>
    
    <button @click="increate(3)">Mutationbutton>

    <h2>Actionh2>
    
    <button @click="increateAsync(6)">Mutationbutton>
  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: {{ num }}p>
    <p>msg: {{ message }}p>
    <h2>Getterh2>
    
    <p>reverseMsg: {{ reverseMsg }}p>

    <h2>Mutationh2>
    
    <button @click="increate(3)">Mutationbutton>

    <h2>Actionh2>
    
    <button @click="increateAsync(6)">Mutationbutton>

    <h2>Modulesh2>
    
    
    <p>products: {{ products }}p>
    <button @click="setProducts([])">Mutationbutton>
  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>

六、严格模式

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>stricth2>
<button @click="$store.state.msg = 'lagou'">strictbutton>

大前端学习--Vuex状态管理_第4张图片

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

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

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

七、购物车案例

1. 模板

地址:https://github.com/goddlts/vuex-cart-demo-template.git

用到了ElementUI、Vuex、Vue-Router

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

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

三个组件:

  • 商品列表组件
  • 购物车列表组件
  • 我的购物车组件(弹出窗口)

2. 商品列表组件

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

3. 我的购物车组件

  • 购买商品列表
  • 统计购物车中的商品数量和价格
  • 购物车上的商品数量
  • 删除按钮

4. 购物车组件

  • 展示购物车列表
  • 全选功能
  • 增减商品功能和统计当前商品的小计
  • 删除商品
  • 统计选中商品和价格

5. Vuex插件介绍

  • Vuex的插件就是一个函数
  • 这个函数接受一个store参数

这个参数可以订阅一个函数,让这个函数在所有的mutation结束之后执行。

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

Store/index.js

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

const myPlugin = store => {
  store.subscribe((mutation, state) => {
    if (mutation.type.startsWith('cart/')) {
      window.localStorage.setItem('cart-products', JSON.stringify(state.cart.cartProducts))
    }
  })
}

export default new Vuex.Store({
  state: {
  },
  mutations: {
  },
  actions: {
  },
  modules: {
    products,
    cart
  },
  plugins: [myPlugin]
})

八、模拟实现Vuex

Myvuex/index.js

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._mutaions = mutations
    this._actions = actions
  }

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

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

function install (Vue) {
  _Vue = Vue
  _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'

你可能感兴趣的:(大前端,Vue)