学习笔记(十八)Vuex状态管理

Vuex状态管理

组件状态管理及组件间通信回顾

状态管理

状态集中管理和分发,解决多个组件共享状态的问题

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

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

单向数据流示意图

image-20210112183703811

组件间通信

Vue常见的组件间通信方式有三种

  • 父组件给子组件传值
  • 子组件给父组件传值
  • 不相关组件之间传值

其他一些通信方式(通常不推荐)

  • $root
  • $parent
  • $children
  • $refs
    • 在普通HTML标签上使用ref,获取到的是DOM
    • 在组件标签上使用ref,获取到的是组件实例

父组件给子组件传值

  • 子组件通过props接收数据
  • 父组件通过给子组件设置相应的属性传值

子组件向父组件传值

  • 子组件通过this.$emit(event, param)方式触发自定义事件,并携带参数,以此向父组件传值
  • 父组件通过给子组件注册相应的自定义事件,并接收事件传递的参数来获取子组件传递的值
    • 父组件在注册子组件自定义事件时,也可以通过$event获取事件传递的参数

不相关组件之间传值

不相关组件之间的传值以eventbus的方式进行

  • 通过eventbus.$on(eventname, (param) => {})注册自定义事件,并在事件被触发时通过注册的回调函数获取参数传递
  • 通过eventbus.$emit(eventname, param)触发自定义事件,传递参数

Vuex

什么是Vuex?

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

什么情况下需要使用Vuex?

非必要的情况下不要使用Vuex,Vuex增加了一些概念,当项目相对简单的时候,使用Vuex会使业务处理变的复杂

  • 大型的单页应用程序可以使用Vuex
    • 多个视图依赖同一状态
    • 不同视图的行为需要变更同一状态

Vuex核心概念

image-20210112231020747

Store

Vuex的核心,每个应用只有一个Store,是存储状态的容器

Vuex通过在Vue根实例中注册store选项,提供了一种机制将状态从根组件注入到每个子组件中(需要使用Vue.use(Vuex)),在子组件中通过this.$store进行访问

State

单一状态树,存储所有的状态数据,并且是响应式的

在组件中可以通过this.$store.state来访问State中的状态,一个简单的访问状态的方式是使用计算属性返回状态的值

当一个组件中需要访问的状态数量较多时,频繁的导入this.$store或者声明计算属性就会显得重复和冗余,Vuex提供了辅助函数mapState来帮助生成计算属性

mapState可以接收对象或者数组作为参数,指定需要访问的状态名称,并返回包含状态对应计算属性的对象

  • 使用示例

    // 引入 mapState 辅助函数
    import { mapState } from 'vuex'
    
    export default {
      // ...
      // 1. 传入对象方式
      computed: mapState({
        // 箭头函数可使代码更简练
        count: state => state.count,
    
        // 传字符串参数 'count' 等同于 `state => state.count`
        // 映射状态别名 countAlias 避免与局部状态名冲突
        countAlias: 'count',
    
        // 为了能够使用 `this` 获取局部状态,必须使用常规函数
        countPlusLocalState (state) {
          return state.count + this.localCount
        }
      })
    
      // 2. 传入数组方式
      computed: mapState([
        // 映射 this.count 为 store.state.count
        'count'
      ])
    
      // 3. 与局部计算属性混用时 使用展开运算符
      computed: {
        localComputed () { /* ... */ },
        // 使用对象展开运算符将此对象混入到外部对象中
        ...mapState({
          // ...
        })
      }
    }
    
    

    tips:使用Vuex并不意味着要将组件所有的状态都放入Store中,组件可以保留自己的局部状态,如果这个状态只属于这个组件

Getter

Getter用于对State做一些处理,并返回处理后的值,有点类似于计算属性

Getter通过Storegetters选项进行定义,接收state作为默认参数,并返回处理后的结果

// 官方示例
const store = new Vuex.Store({
  state: {
    todos: [
      { id: 1, text: '...', done: true },
      { id: 2, text: '...', done: false }
    ]
  },
  getters: {
    doneTodos: state => {
      return state.todos.filter(todo => todo.done)
    }
  }
})
  • Getter也可以接收getters作为第二个参数,来获取其他的定义的Getter

    getters: {
      // ...
      doneTodosCount: (state, getters) => {
        return getters.doneTodos.length
      }
    }
    
  • Getter可以返回一个接收参数的处理函数,在访问时传入参数,并返回相应的处理结果

    getters: {
      // ...
      getTodoById: (state) => (id) => {
        return state.todos.find(todo => todo.id === id)
      }
    }
    

在组件中可以通过this.$store.getters来访问定义的Getter,类似State,一个简单的访问Getter的方式是使用计算属性返回Getter的值

Vuex同样提供了辅助函数mapGetters来帮助生成计算属性,使用方式与mapState类似

  • 使用示例

    import { mapGetters } from 'vuex'
    
    export default {
      // ...
      computed: {
          // 使用对象展开运算符将 getter 混入 computed 对象中
        // 接收数组方式
        ...mapGetters([
          'doneTodosCount',
          'anotherGetter',
          // ...
        ])
        // 接收对象方式
        ...mapGetters({
          // 把 `this.doneCount` 映射为 `this.$store.getters.doneTodosCount`
          doneCount: 'doneTodosCount'
        })
      }
    }
    

    组件中访问State通常也会使用计算属性来返回State的值,那么为什么要使用Getter而不直接使用计算属性呢?

    当多个组件中访问某个State都要进行相同的处理时,将这些处理逻辑集中抽取到Getter中更合适一些

Mutation

Mutation用来对State进行修改,每一个Mutation都包含一个字符串的事件类型(type)回到函数(handler)

  • 所有状态的修改必须通过提交Mutation来完成

  • Mutation必须是同步的,这样才可以追踪状态的变化,不要在Mutation中执行异步操作

Mutation通过storemutations选项进行定义,接收statepayload作为参数,并在其中对state进行修改

const store = new Vuex.Store({
  state: {
    count: 1
  },
  mutations: {
    increment (state, payload) {
      state.count += payload.amount
    }
  }
})

一些小规范

  • 使用常量来定义Mutation的type,并将这些定义的常量放在单独文件统一管理,这样可以使Mutation的定义一目了然

    import Vuex from 'vuex'
    import { SOME_MUTATION } from './mutation-types'
    
    const store = new Vuex.Store({
      state: { ... },
      mutations: {
        // 我们可以使用 ES2015 风格的计算属性命名功能来使用一个常量作为函数名
        [SOME_MUTATION] (state) {
          // mutate state
        }
      }
    })
    
  • payload是调用Mutation时传入的额外参数,可以是任意类型的值,但通常建议传入一个对象,这样可以包含多个字段,且携带了属性名增加了可读性

在组件中可以通过this.$store.commit(mutationType, payload)的方式来调用Mutation,也可以通过传入对象的方式来调用

this.$store.commit({
    type: mutationType,
    ...payload
})

此时传入的整个对象会被当成payload传个相应的Mutation

Vuex提供了mapMutations辅助函数,来帮助我们将mutationscommit调用映射到组件的methods

import { mapMutations } from 'vuex'
 
export default {
  // ...
  methods: {
    ...mapMutations([
      'increment', // 将 `this.increment()` 映射为 `this.$store.commit('increment')`
 
      // `mapMutations` 也支持载荷:
      'incrementBy' // 将 `this.incrementBy(amount)` 映射为 `this.$store.commit('incrementBy', amount)`
    ]),
    ...mapMutations({
      add: 'increment' // 将 `this.add()` 映射为 `this.$store.commit('increment')`
    })
  }
}

Action

Action用来执行异步操作更改状态,通过Action来执行异步操作逻辑,然后调用Mutation来更改状态(所有状态都必须通过Mutation来修改)

Action通过store的选项actions进行定义,接收contextpayload作为参数,执行异步操作,并调用Mutation修改状态

const store = new Vuex.Store({
  state: {
    count: 0
  },
  mutations: {
    increment (state) {
      state.count++
    }
  },
  actions: {
    increment (context) {
      context.commit('increment')
    }
  }
})

context参数是一个对象,具有与store实例相同的属性与方法,因此可以通过context.commit()来提交一个Mutation

在组件中,通过使用this.$store.dispatch(action, payload)来分发一个Action

Vuex提供了mapActions辅助函数,帮助我们将对Action的dispatch调用映射到组件methods

import { mapActions } from 'vuex'
 
export default {
  // ...
  methods: {
    ...mapActions([
      'increment', // 将 `this.increment()` 映射为 `this.$store.dispatch('increment')`
 
      // `mapActions` 也支持载荷:
      'incrementBy' // 将 `this.incrementBy(amount)` 映射为 `this.$store.dispatch('incrementBy', amount)`
    ]),
    ...mapActions({
      add: 'increment' // 将 `this.add()` 映射为 `this.$store.dispatch('increment')`
    })
  }
}

Module

Vuex使用单一状态树,当项目变的复杂,Store中存储的状态数量变的庞大而臃肿,这时可以将Store分割成多个Module,每个Module拥有自己的State、Mutation、Action

const moduleA = {
  state: () => ({ ... }),
  mutations: { ... },
  actions: { ... },
  getters: { ... }
}
 
const moduleB = {
  state: () => ({ ... }),
  mutations: { ... },
  actions: { ... }
}
 
const store = new Vuex.Store({
  modules: {
    moduleA,
    moduleB
  }
})
 
store.state.moduleA.xxx // -> moduleA 的状态
store.state.moduleB.xxx // -> moduleB 的状态

对于Module的Mutation

  • 第一个参数state是Module的局部state

对于Module的Getter

  • 第一个参数state是Module的局部state
  • 第二个参数getters是Module的局部getters
  • 第三个参数rootState是根节点状态

对于Module的Action

  • context.rootState是根节点状态

默认情况下,模块的action、mutation、getter注册在全局命名空间,可以通过为模块添加namespace: true选项为模块开启独立命名空间

对于开启了命名空间的模块,在组件中访问其state、action、mutation、getter时,需要额外添加命名空间,对于使用辅助函数来映射访问时,可以将命名空间字符串作为第一个参数传给辅助函数,例如

computed: {
  ...mapState('moduleA', {
    a: state => state.a,
    b: state => state.b
  })
},
methods: {
  ...mapActions('moduleA', [
    'foo', // -> this.foo()
    'bar' // -> this.bar()
  ])
}

也可以使用createNamespacedHelpers创建基于某个命名空间的辅助函数

import { createNamespacedHelpers } from 'vuex'
 
const { mapState, mapActions } = createNamespacedHelpers('moduleA')
 
export default {
  computed: {
    // 在 `moduleA` 中查找
    ...mapState({
      a: state => state.a,
      b: state => state.b
    })
  },
  methods: {
    // 在 `moduleB` 中查找
    ...mapActions([
      'foo',
      'bar'
    ])
  }
}

模块的动态注册

在Store创建之后,可以通过store.registerModule方法动态注册模块

import Vuex from 'vuex'
 
const store = new Vuex.Store({ /* 选项 */ })
 
// 注册模块 `myModule`
store.registerModule('myModule', {
  // ...
})
// 注册嵌套模块 `nested/myModule`
store.registerModule(['nested', 'myModule'], {
  // ...
})

严格模式

我们约定,所有对State的修改都应当通过Mutation来进行

然而在代码层面,并没有强制限制对State的直接修改

我们可以通过storestrict: true选项来开启严格模式,在严格模式下,所有非Mutation引起的状态变更,都将在控制台抛出错误(但状态仍旧会被修改

不要在生产环境下启用严格模式,严格模式会深度监测状态树来检测不合规范的状态变更,造成性能损耗

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

Vuex使用案例

这里通过实现一个购物车案例来演示Vuex的使用

购物车案例模板项目 https://github.com/goddlts/vuex-cart-demo-template.git

使用Vuex实现后的项目 https://gitee.com/rpyoyo/vuex-cart-demo-template.git

基本功能

  • 商品列表
    • 展示商品的列表
    • 可以通过加入购物车按钮将相应的商品加入购物车中,购物车中没有该商品则增加商品,如果已有则增加数量
image-20210122214432272
  • 我的购物车
    • 展示添加到购物车中的商品列表
    • 通过删除按钮可以移除指定商品
    • 汇总商品总数及价格
image-20210122214501714
  • 购物车列表
    • 展示购物车中的商品列表
    • 通过删除按钮可以移除指定商品
    • 可以改变商品的数量
    • 可以单独勾选、全选商品(全选的状态根据商品勾选情况变化)
    • 汇总当前已勾选的商品的数量及价格(与我的购物车中的不同)
image-20210122214517191

具体实现

通过分析基本功能,将整个状态分割成商品products与购物车cart两个模块

store对应目录下创建modules目录,并分别新建products.jscart.js两个模块文件,分别定义相应的stategettersmutationsactions,开启命名空间,并使用export default导出

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

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

storemodules选项中注册模块

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

Vue.use(Vuex)

const store = new Vuex.Store({
  state: {},
  mutations: {},
  actions: {},
  modules: {
    cart,
    products
  },
})

products模块的定义与实现

// store/modules/products.js
import axios from 'axios'

const state = {
  products: []
}
const getters = {}
const mutations = {
  setProducts (state, payload) {
    state.products = payload
  }
}
const actions = {
  async getProducts ({ commit }) {
    const { data } = await axios.get('http://127.0.0.1:3000/products')
    commit('setProducts', data)
  }
}

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

cart模块的定义与实现

// store/modules/cart.js
const state = {
  cartProducts: []
}
const getters = {
  totalCount ({ cartProducts }) {
    return cartProducts.reduce((a, b) => a + b.count, 0)
  },
  totalPrice ({ cartProducts }) {
    return cartProducts.reduce((a, b) => a + b.count * b.price, 0)
  },
  checkedCount ({ cartProducts }) {
    return cartProducts.reduce((a, b) => a + (b.isChecked ? b.count : 0), 0)
  },
  checkedPrice ({ cartProducts }) {
    return cartProducts.reduce((a, b) => a + (b.isChecked ? b.count * b.price : 0), 0)
  }
}
const mutations = {
  addToCart (state, payload) {
    // 1. 没有该商品,添加至购物车,设置默认数量,是否勾选等属性
    // 2. 如果已有该商品,增加数量
    const product = state.cartProducts.find(item => item.id === payload.id)
    if (product) {
      product.count++
      product.totalCount += product.price
    } else {
      state.cartProducts.push({
        ...payload,
        count: 1,
        isChecked: true,
        totalPrice: payload.price
      })
    }
  },
  deleteFromCart (state, payload) {
    const index = state.cartProducts.findIndex(item => item.id === payload.id)
    index !== -1 && state.cartProducts.splice(index, 1)
  },
  updateAllProductsChecked (state, payload) {
    state.cartProducts.forEach(element => {
      element.isChecked = payload.checked
    })
  },
  updateProductChecked (state, payload) {
    const product = state.cartProducts.find(item => item.id === payload.id)
    product && (product.isChecked = payload.checked)
  },
  updateProduct (state, payload) {
    const product = state.cartProducts.find(item => item.id === payload.id)
    if (product) {
      product.count = payload.count
      product.totalPrice = product.count * product.price
    }
  }
}
const actions = {}

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

在相应的Vue组件中,通过Vuex辅助函数,将定义的stategettersmutations映射到computedmethods

cart.vue组件为例

// cart.vue
...

本地存储(持久化)

通过Vuex中定义了状态并在Vue组件中使用,基本的状态管理功能就完成了,但当我们刷新页面时,之前对状态的操作就会丢失,这个时候就需要对状态进行持久化,可以将状态通过localStorage进行本地存储

  • 如果项目比较简单,我们可以针对单独的模块对其状态进行存储与使用

  • 如果项目比较复杂,我们通常会对整个单一状态树进行存储,例如使用vuex-persist之类的第三方库

持久化实现原理

我们需要在Vuex状态初始化的时候,将我们存储的状态恢复到store中,同时我们需要在每次状态发生变化时,将新的状态更新到存储中,我们知道Vuex的状态必须通过mutation来进行修改,我们可以使用store.subscribe((mutation, state) => {}),它会在每次mutation完成后执行,并获取到更新后的状态

// 定义一个 persist 持久化插件
const persist = (store) => {
  store.subscribe((mutation, state) => {
    // 将更新后的 state 存入 localStorage
    localStorage.setItem('vuex-persist', JSON.stringify(state))
  })
}

const store = new Vuex.Store({
  state: {},
  mutations: {},
  actions: {},
  modules: {
    cart,
    products
  },
  plugins: [persist] // plugins 选项中注册插件
})

// 从 localStorage 中读取保存的 state
const state = JSON.parse(localStorage.getItem('vuex-persist'))
// store 实例创建后替换 state
state && store.replaceState(state)

export default store

Vuex简单模拟实现

基本结构

通过对Vuex基本使用的分析,我们可以知道Vuex的基本结构

  • Vue.use(Vuex)
    • Vuex是一个对象,需要通过Vue.use()进行注册,因此需要实现install方法
  • const store = new Vuex.Store({ state, getters, mutations, actions })
    • Vuex对象包含Store类,用于创建store实例,接收stategettersmutationsactions等选项
  • $store
    • 组件中通过$storestate等的访问,commit()提交mutationdispatch分发action
// myvuex/index.js
// Vuex基本结构

let _Vue = null

class Store {

}

const install = (Vue) {
    _Vue = Vue
}

export default {
    Store,
    install
}

install

const install = (Vue) => {
  _Vue = Vue
  _Vue.mixin({
    beforeCreate () {
      if (this.$options.store) {
        // 挂载 Vue 选项上的 store 到 Vue 原型上
        _Vue.prototype.$store = this.$options.store
      }
    }
  })
}

Store类

class Store {
  constructor (options) {
    const {
      state = {},
      getters = {},
      mutations = {},
      actions = {}
    } = options
    // state 是响应式的
    this.state = _Vue.observable(state)
    this.getters = Object.create(null)
    // 转换 getters[key] 为 $store.getters.key 的访问方式
    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)
  }
}

你可能感兴趣的:(学习笔记(十八)Vuex状态管理)