vuex数据流管理

347 阅读3分钟

该笔记参考拉勾教育前端课程整理:kaiwu.lagou.com/

vuex

  • Vue组件间通信方式回顾

  • Vuex核心概念和基本使用回顾

  • 购物车案例

  • 模拟实现Vuex

组件内的状态管理流程

Vue核心功能:数据驱动和组件化

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

Vue组件间通信方式回顾

父组件给子组件传值

  • 子组件中通过props接收数据
  • 父组件中给子组件通过相应属性传值
<!--父:注册子组件-->
<child title="My journey with Vue"></child>
//子
  props: {
    title: String
  }

子组件给父组件传值

  1. 父:注册事件
<child :fontSize="hFontSize" v-on:enlargeText="enlargeText"></child>
<child :fontSize="hFontSize" v-on:enlargeText="hFontSize += $event"></child>
enlargeText (size) {
	this.hFontSize += size
}
  1. 子:通过$emit传递参数
<button @click="handler">文字增大</button>
handler () {
      //this:当前子组件对象
      this.$emit('enlargeText', 0.1)
    }

不相关组件之间传值

使用Event Bus作为事件中心

  1. eventbus.js
//调用它的$emit和$on
import Vue from 'vue'
export default new Vue()
  1. 组件一:调用$emit发布
bus.$emit('numchange', this.value)
  1. 组件二:调用$on订阅
bus.$on('numchange', (value) => {
      this.msg = `您选择了${value}件商品`
    })

通过ref获取子组件

  • $root
  • $parent
  • $children
  • $refs
    • 在普通HTML标签上使用ref,获取到的是DOM
    • 在组件标签上使用ref,获取到的是组件实例
<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()
}

简易的状态管理方案

  • 如果多个组件之间要共享状态(数据),多个组件之 间互相传值很难跟踪数据的变化,如果出现问题很难定位问题
  • 多个视图依赖于同一状态
  • 来自不同视图的行为需要变更同一状态
  • 把组件的共享状态抽取出来,以一个全局单例模式管理
  1. 创建一个共享的仓库 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
}
}
  1. 把共享的仓库 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 是一个专为 Vue.js 应用程序开发的状态管理模式

它采用集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化

Vuex 是专门为 Vue.js 设计的状态管理库,以利用 Vue.js 的细粒度数据响应机制来进行高效的状态更新

把组件的共享状态抽取出来,以一个全局单例模式管理(不管在树的哪个位置,任何组件都能获取状态或者触发行为)

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

使用场景

大型单页应用程序:

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

eg:购物车

核心概念

  • Store:容器,包含应用中大部分状态,要通过提交Mutation方式改变状态
  • State:状态,响应式,保存在Store中,状态是唯一的,称为单一状态树。后续+模块化。
  • Getter:计算属性,方便从一个属性派生出其它的值,内部可以对计算的结果进行缓存
  • Mutation:状态变化需要过提交Mutation
  • Action:类似Mutation,还可以进行异步操作。内部改变状态时都要提交Mutation
  • Module:模块。使用单一状态树,所有对象会集中在一个对象树上,应用十分复杂时,对象会变得臃肿;可以将Store划分为模块,每个模块有自己的State、Mutation、Action

结构分析

  • 导入 Vuex
import Vuex from 'vuex'
  • 注册 Vuex
//store\index.js
Vue.use(Vuex)
export default new Vuex.State({
    state:{
        
    },
    mutations:{
        
    },
    modules:{
        
    }
})
  • 注入 $store 到 Vue 实例
//app.js
import store from './store'
new Vue({
    store
}).$mount('#app')

State

  • 在组件中绑定状态
  1. store\index.js
//store\index.js
import Vuex from 'vuex'

Vue.use(Vuex)

export default new Vuex.Store({
  state: {
    count: 0,
    msg: 'Hello Vuex'
  },
})
  1. app.vue
<!--app.vue-->
<div>
    <!--count:{{ $store.count }}-->
    msg:{{ message }}
</div>

<script>
    //简化模板内容
	import { mapState } from 'vuex'
    export default{
          computed: {
    // count: state => state.count
              //映射状态属性
            ...mapState({ num: 'count', message: 'msg' }),
		},
    }
</script>
  • Vuex 使用单一状态树,用一个对象就包含了全部的应用层级状态
  • 使用 mapState 简化 State 在视图中的使用,mapState 返回计算属性

Getter

  • Getter 就是 store 中的计算属性,使用 mapGetter 简化视图中的使用
  • 意思就是在状态内部使用它去处理属性
  1. store\index.js
////store\index.js
getters: {
    reverseMsg (state) {
      return state.msg.split('').reverse().join('')
    }
  },

join() 方法會將陣列(或一個類陣列(array-like)物件)中所有的元素連接、合併成一個字串,並回傳此字串

  1. app.vue
<!--app.vue-->
<div>
    reverseMsg: {{ reverseMsg }}
</div>

<script>
    //简化模板内容:可以传数组和对象
	import { mapGetters } from 'vuex'
    export default{
          computed: {
              ...mapGetters(['reverseMsg']),
		},
    }
</script>

Mutaion

更改 Vuex 的 store 中的状态的唯一方法是提交 mutation。Vuex 中的 mutation 非常类似于事件:每个 mutation 都有一个字符串的 事件类型 (type) 和 一个 回调函数 (handler)。这个回调函数就是我们实际进行状态更改的地方,并且它会接受 state 作为第一个参数

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

  1. store\index.js
mutations: {
    increate (state, payload) {
      state.count += payload
    }
  },
  1. app.vue
<!--app.vue-->
<div>
    <!--count的值会加2
	 <button @click="$store.commit('increate', 2)">Mutation</button>
</div>
	-->
    <button @click="increate(3)">Mutation</button>
</div>

<script>
    //简化模板内容
	import { mapMutations } from 'vuex'
    export default{
          methods: {
            ...mapMutations(['increate']),
          }
    }
</script>

Action

Action 类似于 mutation,不同在于:

  • Action 提交的是 mutation,而不是直接变更状态
  • Action 可以包含任意异步操作
  1. store\index.js
actions: {
    increateAsync (context, payload) {
      setTimeout(() => {
        context.commit('increate', payload)
      }, 2000)
    }
  },
  1. app.vue

    Action 通过 store.dispatch 方法触发:

<!--app.vue-->
<div>
    <!--count的值会加2
	 <button @click="$store.dispatch('increateAsync', 6)">Mutation</button>
</div>
	-->
	<button @click="increateAsync(6)">Action</button>
</div>

<script>
    //简化模板内容
	import { mapActions } from 'vuex'
    export default{
          methods: {
            ...mapActions(['increateAsync']),
          }
    }
</script>

Module

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

  1. 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
}
  1. store\index.js:导入两个模块
import products from './modules/products'
import cart from './modules/cart'

modules: {
    products,
    cart
  }

通过store.state.xxx访问模块成员

  1. app.vue
<!--app.vue-->
<div>
    <!-- products: {{ $store.state.products.products }} <br>
    <button @click="$store.commit('setProducts', [])">Mutation</button> -->
    products: {{ products }} <br>
	<button @click="setProducts([])">Mutation</button>
</div>

<script>
    //简化模板内容
	import { mapMutations } from 'vuex'
    export default{
          methods: {
              //1.命名空间 2.映射属性
            ...mapMutations('products', ['setProducts'])
          }
    }
</script>

严格模式

const store = new Vuex.Store({
  // ...
  strict: true
})

在严格模式下,无论何时发生了状态变更且不是由 mutation 函数引起的,将会抛出错误。这能保证所有的状态变更都能被调试工具跟踪到

e.g:浏览器会报错,但数据还是会被修改

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

  • 不要在生产环境下开启严格模式,它会:

严格模式会深度监测状态树来检测不合规的状态变更——请确保在发布环境下关闭严格模式,以避免性能损失

状态树:用一个对象就包含了全部的应用层级状态

  • 构建工具来处理,webpack打包时通过环境变量去处理
const store = new Vuex.Store({
  // ...
  strict: process.env.NODE_ENV !== 'production'
})

购物车案例

功能列表

  • 商品列表组件
  • 商品列表中弹出框组件
  • 购物车列表组件

商品列表

  • Vuex 中创建两个模块,分别用来记录商品列表购物车的状态,store 的结构:

商品列表功能-Products.js

  • products 模块,store/modules/products.js

  • store/index.js 中注册 products.js 模块

    export default new Vuex.Store({
      modules: {
        products,
        cart
      },
    })
    
  • views/products.vue 中实现商品列表的功能

添加购物车-cart.js

  • cart 模块实现添加购物车功能,store/modules/cart.js
  • store/index.js 中注册 cart 模块
  • view/products.vue 中实现添加购物车功能

弹出购物车窗口

  • src\components\pop-cart.vue
  1. 展示购物车列表

    ...mapState('cart', ['cartProducts']),
    
  2. 删除

    src\store\modules\cart.js

    ...mapMutations('cart', ['deleteFromCart'])
    
  3. 小计

    src\store\modules\cart.js

    ...mapGetters('cart', ['totalCount', 'totalPrice'])
    

购物车

购物车列表

……

全选功能

  • cart 模块实现更新商品的选中状态,store/modules/cart.js

Vuex插件介绍

  • Vuex的插件就是一个函数
  • 这个函数接收一个store的参数
const myPlugin = store => {
    //当store初始化后调用
	store.subscribe((mutain, state) => {
        //每次mutation之后调用
        //mutation的格式为{type, payload}
    })
}
const store = new Vuex.Store({
	plugins: [myPlugin]
})

以该项目为例:src\store\index.js

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

简单模拟Vuex

初始化Vuex架构

  • src\myvuex\index.js
let _Vue = null
class Store { }

//vue-router类似
//vue构造函数
function install(Vue) {
    _Vue = Vue
}

export default {
    Store,
    install
}
  • install
//vue-router类似
function install(Vue) {
    _Vue = Vue
    //通过混入beforeCreate获取到vue实例
    _Vue.mixin({
        beforeCreate(){
            if(this.$options.store) {
                _Vue.prototype.$store = this.$options.store
            }
        }
    })
}
  • store
class Store {
    constructor(options) {
        const {
            state = {},
            getter = {},
            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)
    }
}