vue2中vuex状态管理的理解(菜单面包板)

vue2中vuex状态管理的理解(菜单面包板),第1张

本片理解基于vue2对应的Vuex文档,结合了官网文档以及众多前辈大佬所发布的帖子,由衷表示感谢。

vuex的超详细讲解和具体使用细节记录


随着我们进一步扩展约定,即组件不允许直接变更属于 store 实例的 state,而应执行 action 来分发 (dispatch) 事件通知 store 去改变,最终达成了 Flux 架构。这样约定的好处是,能够记录所有 store 中发生的 state 变更,同时实现能做到记录变更、保存状态快照、历史回滚/时光旅行的先进的调试工具。

一、什么是Vuex?

Vuex是一个专门为Vue.js应用程序所设计开发出来的一个状态管理模式和库。它采用了集中式存储管理应用的所有组件状态,并且对于状态的变更建立了一套完整的使用规则,保证了状态变化的可预测性和安全性。

状态管理模式:类似于24种程序设计模式一样,在我们解决某一类特殊问题上提供了一种较为合理的参考方案和编程实现方案。库:对于库的理解就是两个词集成和封装,在解决这里问题的时候,不用你用最原始的数据结构和算法来一一实现每一个功能,站在巨人的肩膀上,直接调用库中集成好的API就可以了。状态:状态即驱动应用的数据源(data)。我们知道灯泡开和关分别是一种固定状态,我们可用1代表开,0代表关。这样的话,用数字就可以代表状态了,那反过来说,状态就是数据的具体表现形式,所以我们可以这样理解:状态管理就是数据管理,进一步而言,vuex就是去管理vue中的数据的 二、vuex的使用场景有哪些?解决了什么样的问题?

这是一个简单的单项数据流示意图,在单个组件中数据-视图- *** 作构成一个回环。但是实际项目中可能需要我们打通组件之间(多个.vue页面)数据不互通的特点,类似于组件之间的通信,除了props,emit、refs、v-model、provide/inject、eventBus外就是vuex了,前面的几种方式在实现通信(数据共享)时, *** 作比较麻烦维护起来困难。由此,vuex将组件共享的状态(数据)单独抽取为一个全局的单例模式进行管理,这样无论项目中的哪一个组件都可以通过我们制定的规则,全局访问、 *** 作我们的全局单例状态。

需要特别注意的是Vuex 和单纯的全局对象有以下两点不同:

Vuex 的状态存储是响应式的。当 Vue 组件从 store 中读取状态的时候,若 store 中的状态发生变化,那么相应的组件也会相应地得到高效更新。相较于其他Flux实现,vuex是一个专门为vue.js设计开发的,融合了vue.js细粒度数据响应机制来进行高效的状态更新。你不能直接改变 store 中的状态。改变 store 中的状态的唯一途径就是显式地提交 (commit) mutation。这样使得我们可以方便地跟踪每一个状态的变化,从而让我们能够实现一些工具帮助我们更好地了解我们的应用。

开发大型单页应用可能会考虑如何更好地在组件外部管理状态,Vuex 将会成为自然而然的选择。如果您不打算开发大型单页应用,使用 Vuex 可能是繁琐冗余的。确实是如此——如果您的应用够简单,您最好不要使用 Vuex。一个简单的 store 模式就足够您所需了。vuex就像一个仓库,用来存放组件中需要用到的数据,至于管理,就是增删改查,往vuex中存取、修改、删除等 *** 作

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

vuex的应用场景

正常数据放到data里面就行了,免得麻烦,一般小项目都很少用到vuex,毕竟vuex的步骤稍微多一点一般公共数据放到vuex中会比较好,毕竟组件有自己的data可以存放数据,公共的变量如用户信息、token等。vuex存一份,localstorage再存一份,取数据直接从vuex里面取,取不到再从localstorage里面去取。跨很多组件层级的数据通信,也可以通过vuex去做管理,毕竟vuex就像一个对象一个,好多组件都指向这个对象,当这个vuex对象发生了变化,所有的组件中对应的内容都会发生变化,这样就能做到实时响应,一个变,大家都变。 三、vuex的基本组成和对应作用

state

Vuex使用单一状态树,每个应用将仅仅包含一个 store 实例,即一个对象就包含了整个应用程序所有使用到的状态,单一状态树让我们能够直接地定位任一特定的状态片段,在调试的过程中也能轻易地取得整个当前应用状态的快照。

我们在组件中读取store中的状态一般在computed:{}计算属性中进行,这样每当状态发生变化的时候,都会自动求取计算属性,更新相关DOM。我们可以将同样的函数定义为一个方法,而不是一个计算属性。从最终结果来说,这两种实现方式确实是完全相同的。然而,不同的是计算属性将基于它们的响应依赖关系缓存。计算属性只会在相关响应式依赖发生改变时重新求值。这就意味着只要 author.books 还没有发生改变,多次访问 publishedBookMessage 时计算属性会立即返回之前的计算结果,而不必再次执行函数。

注意:在每一个组件想要访问状态管理全局唯一实例时都需要import store from ‘store’,如果很多组件都有使用到,每个页面都插入一下,属实拉跨。Vuex 通过 Vue 的插件系统将 store 实例从根组件中“注入”到所有的子组件里。且子组件能通过 this.$store 访问到。(全局main.js配置)

当一个组件需要获取多个状态的时候,将这些状态都声明为计算属性会有些重复和冗余。为了解决这个问题,我们可以使用 mapState 辅助函数帮助我们生成计算属性,让你少按几次键:

 computed: mapState({
    // 箭头函数可使代码更简练
    count: state => state.count,

    // 传字符串参数 'count' 等同于 `state => state.count`
    countAlias: 'count',
 })
//当计算属性的名称和state中某个子节点相同,可以直接给mapState传对应字符串就好了。

getter

这个方法可以理解为我们需要获取某一状态集合中的部分值(子集),派生出一些状态,如果有多个组件需要用到此属性,我们要么复制这个函数,或者抽取到一个共享函数然后在多处导入它——无论哪种方式都不是很理想。由此推出了getter,将 *** 作集成到store中

Getter 会暴露为 store.getters 对象,你可以以属性的形式访问这些值。getter 在通过属性访问时是作为 Vue 的响应式系统的一部分缓存其中的。

Getter 接受 state 作为其第一个参数:

const store = createStore({
  state: {
    todos: [
      { id: 1, text: '...', done: true },
      { id: 2, text: '...', done: false }
    ]
  },
  getters: {
    doneTodos: (state) => {
      return state.todos.filter(todo => todo.done)
    }
  }
})

Getter 也可以接受其他 getter 作为第二个参数:

getters: {
  // ...
  doneTodosCount (state, getters) {
    return getters.doneTodos.length
  }
}

通过方法访问,getter 在通过方法访问时,每次都会去进行调用,而不会缓存结果。

可以通过让 getter 返回一个函数,来实现给 getter 传参。在你对 store 里的数组进行查询时非常有用。

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

store.getters.getTodoById(2) // -> { id: 2, text: '...', done: false }

mapGetters 辅助函数仅仅是将 store 中的 getter 映射到局部计算属性:

import { mapGetters } from 'vuex'

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

Mutation

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

const store = createStore({
  state: {
    count: 1
  },
  mutations: {
    increment (state) {
      // 变更状态
      state.count++
    }
  }
})

你不能直接调用一个 mutation 处理函数。这个选项更像是事件注册:“当触发一个类型为 increment的 mutation 时,调用此函数。”要唤醒一个 mutation 处理函数,你需要以相应的 type 调用 store.commit 方法:注意使用commit来获取使用权限就可以了。

除此之外外,mutation中函数还支持第二个参数,在这里叫做载荷,载荷的类型在多数情况下是一个对象,这样可以包含多个属性记录,在组件中使用方式形如:

store.commit('increment', {
  amount: 10
})

一条重要的原则就是要记住 mutation 必须是同步函数,混合异步调用会导致你的程序很难调试.

使用 mapMutations 辅助函数将组件中的 methods 映射为 store.commit 调用(需要在根节点注入 store

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 类似于 mutation,不同在于:

Action 提交的是 mutation,而不是直接变更状态。Action 可以包含任意异步 *** 作。

还记得 mutation 必须同步执行这个限制么?Action 就不受约束!我们可以在 action 内部执行异步 *** 作:

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

Action 函数接受一个与 store 实例具有相同方法和属性的 context 对象,因此你可以调用 context.commit 提交一个 mutation,或者通过 context.statecontext.getters 来获取 state 和 getters。

实践中,我们会经常用到 ES2015 的参数解构来简化代码(特别是我们需要调用 commit 很多次的时候):

一开始的时候建议还是按照上面方法的写法,虽然重复了点,但是便于掌握要点。下面这个利用参数解构简化代码,从中结构出commit,来进行的 *** 作。

actions: {
  increment ({ commit }) {
    commit('increment')
  }
}

Action 通过 store.dispatch 方法触发,同时Actions 支持同样的载荷方式和对象方式进行分发:

//异步 *** 作
actions: {
  incrementAsync ({ commit }) {
    setTimeout(() => {
      commit('increment')
    }, 1000)
  }
}

组件中使用Action分发时添加参数的两种方式:

// 以载荷形式分发
store.dispatch('incrementAsync', {
  amount: 10
})

// 以对象形式分发
store.dispatch({
  type: 'incrementAsync',
  amount: 10
})

在组件中使用 this.$store.dispatch('xxx') 分发 action,或者使用 mapActions 辅助函数将组件的 methods 映射为 store.dispatch 调用(需要先在根节点注入 store):

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')`
    })
  }
}

Action 通常是异步的,那么如何知道 action 什么时候结束呢?更重要的是,我们如何才能组合多个 action,以处理更加复杂的异步流程?,Promise 是抽象的异步处理对象,可以简单的理解为Promise 实例生成以后,可以用 then 方法和 catch 方法分别指定 resolved 状态和 rejected 状态的回调函数,即成功的回调和失败的回调。

store.dispatch('actionA').then(() => {
  // ...
})

这是曾经在项目中使用过示例:比较不完整,但是有使用到辅助函数来进行结构

<script>
import { mapState, mapMutations } from 'vuex'
export default {
  computed: { 
    ...mapState({
      tags: (state) => state.table.tabsList,
    }),
  },
  methods: {
    ...mapMutations({
      close: 'closeTag',
    }),
    changeMenu(item) {
      this.$router.push({ name: item.name })
      this.$store.commit('selectMenu', item)
    },
    handleClose(tag, index) {
      let length = this.tags.length - 1
      this.close(tag)
      if (tag.name !== this.$route.name) {
        return
      }
      if (index === length) {
        this.$router.push({
          name: this.tags[index - 1].name,
        })
      } else {
        this.$router.push({ name: this.tags[index].name })
      }
    },
  },
}
</script>

Model

​ 关于模块,可以理解为多个子项目的状态管理,因为状态树是全局唯一的,所以以模块的方式进行划分开来。需要注意的是命名问题。

四、搭建一个完整的vuex基本使用Demo

牢记 Vuex 并不限制你的代码结构。但是,它规定了一些需要遵守的规则:

应用层级的状态应该集中到单个 store 对象中。提交 mutation 是更改状态的唯一方法,并且这个过程是同步的。异步逻辑都应该封装到 action 里面。

首先是我们的目录管理层级:

​ 每一个模块我们都写一个单独的js文件,通过export default { state{},mutations{},getter{},action{}}导出对象,在主配置文件中index.js中插入模块。配置信息如下:

//vue2
import Vue from 'vue'
import Vuex from 'vuex'
import table from'./table'
Vue.use(Vuex)

export default new Vuex.Store({
  modules: {
    table
  }
})
//vue3
import { createStore}from 'vuex'
import table from'./table'

export default createStore({
    modules:{
        table
    }
})

无疑,上面的搭建配置信息是最简单,最容易使用的。当你的项目比较大的时候就可以考虑一下分割相关代码到模块中。实现如官方所推荐的层级结构:

├── index.html
├── main.js
├── api
│   └── ... # 抽取出API请求
├── components
│   ├── App.vue
│   └── ...
└── store
    ├── index.js          # 我们组装模块并导出 store 的地方
    ├── actions.js        # 根级别的 action
    ├── mutations.js      # 根级别的 mutation
    └── modules
        ├── cart.js       # 购物车模块
        └── products.js   # 产品模块
vue2简单的状态管理实例

​ 推荐参考前辈 :该例子通过一个msg作为状态量全局存储在单例树上,在一个组件中通过this.$store.(action->mutation->state)进行读取和修改。前辈在博客里面的内容是很清晰了,每一步的介绍也很精简,我就不造轮子了。下面我会通过实现一个菜单的面包板,来对状态管理做一个介绍。

效果图:

第一步:npm下载

npm install vuex --save

第二步:创建store文件夹,并在index.js下引用,和全局挂载。

在main.js中需要将刚刚创建的文件引入,并挂载到全局vue实例上。

第三步,建立一个以菜单为元素的状态模块 table.js,完成后再index.js中添加,并在添加到modules中。

export default {
    //导出结构说明 一个模块的状态管理应该是 包含state(必选)、mutations(必选)、getter(可选)、action(可选异步)等对象
    state: {
        //控制侧边栏收起,这个读者可以忽略
        isCollapse: false,
        //用于显示面包屑当前所在页面,这个读者可以忽略
        currentMenu: null,
        // tabList用来存放已经点击打开的页面菜单,即面包板
        tabsList: [
        ]
    },
    mutations: {
        //可以直接 *** 作状态,方法的收个参数为当前状态对象,第二个可选参数为载荷,一般为对象
        isCollapseMenu(state) {
            state.isCollapse = !state.isCollapse
        },
        //这个函数是这样运行的,首先在菜单栏目里点击某一个菜单选型,跳转后,全局方法调用来执行下面函数
        //并传入点击变量item,在这个函数体里面有两个修改state的内容(全局变量) 包括应用于顶部面包屑的currentMenu的值
        //以及页面标签页【用一个对象数组来存储标签内容】,先判断是否已经存在,如果不存在着就push进去·,否则不做 *** 作【三目运算】
        selectMenu(state, val) {
            //val.name==='home'?(state.currentMenu=null):state.currentMenu=val
            if (val.name == 'workerhome') {
                state.currentMenu = null;
            }
            else{
                state.currentMenu=val
                //新增tabslist
                let result=state.tabsList.findIndex(item=>item.name==val.name)
                result===-1?state.tabsList.push(val):''
            }
        },
        //用作标签页的删除
        closeTag(state,val){
            let result=state.tabsList.findIndex(item=>item.name===val.name)
            state.tabsList.splice(result,1)
        },
        // 删除所有,用于在用户推出时清空状态
        deleteAll(state){
            state.tabsList=[];
        },
    },
    getter:{
        // getter中我们可以定义一个函数,这个函数我们用来修改state中的值的,函数接收一个参数state,
        //这个state参数就是当前的state对象,通过这个参数可以加工state中的数据,加工好return出去,以供组件中使用
        gettersChange(state){
            return state.tabsList;
        }
    },
    action:{
        //有需要异步 *** 作的异步请求的时候需要在这个对象中添加方法
    }
}

第四步,既然我们对菜单的状态管理的定义和 *** 作已经完成,下面就是添加和展示部分了。

      menu: [
        {
          // lable:菜单名字 name 
          path: '/',
          name: 'home',
          label: '首页',
          icon: 's-home',
          url: 'Home/Home',
        },
        {
          path: '/user',
          name: 'user',
          label: '客户信息管理',
          icon: 'user',
          url: 'User/userManage',
        },
        {
          path: '/worker',
          name: 'worker',
          label: '员工信息管理',
          icon: 'user',
          url: 'Worker/workerManage',
        },
        {
          path: '/project',
          name: 'project',
          label: '项目信息管理',
          icon: 'video-play',
          url: 'Project/projectManage',
        },
        {
          path: '/client',
          name: 'client',
          label: '用户账号管理',
          icon: 'user',
          url: 'Client/clientManage',
        },
        {
          path: '/Workerhome',
          name: 'workerhome',
          label: '员工首页',
          icon: 'user',
          url: 'WorkerHome/workerhome',
        },
        {
          path: '/projectMerberManage',
          name: 'projectMerberManage',
          label: '项目成员管理',
          icon: 'user',
          url: 'Project/projectMerberManage',
        },
        {
          label: '模型主管项目任务管理',
          icon: 'user',
          children: [
            {
              path: '/modelMerberManage',
              name: 'modelMerberManage',
              label: '分配任务',
              icon: 'setting',
              url: 'Model/modelMerberManage',
            },
            {
              path: '/modelApproval',
              name: 'modelApproval',
              label: '审批任务',
              icon: 'setting',
              url: 'Model/modelApproval',
            },
            {
              path: '/modelHistory',
              name: 'modelHistory',
              label: '处理历史',
              icon: 'setting',
              url: 'Model/modelHistory',
            },
          ],
        },
        {
          label: '其他主管项目任务管理',
          icon: 'user',
          children: [
            {
              path: '/otherMerberManage',
              name: 'otherMerberManage',
              label: '分配任务',
              icon: 'setting',
              url: 'Other/otherMerberManage',
            },
            {
              path: '/otherApproval',
              name: 'otherApproval',
              label: '审批任务',
              icon: 'setting',
              url: 'Other/otherApproval',
            },
            {
              path: '/otherHistory',
              name: 'otherHistory',
              label: '处理历史',
              icon: 'setting',
              url: 'Other/otherHistory',
            },
          ],
        },
      ],

上面是我们定义的侧边栏菜单选项,需要注意的的是我们后面的路由使用的是this.$router.push({name:item})的形式,所以菜单的name值一定要和路由中你所设定的name值相等。不然会出现无法找到对应路由的错误。

侧边栏的页面效果实现,怎么样都可以,唯独要注意的 *** 作时每一个具有实际地址意义的菜单都要绑定下面这个函数。什么是实际地址意义呢?即有路由的地址。

    clickMenu(item) {
      this.$router.push({ name: item.name })
      this.$store.commit('selectMenu', item)
    },

第五步,将菜单状态管理的列表数据在一个组件中展示出来

<template>
  <div class="tabs">
    <el-tag
      class="mytag"
      v-for="(tag,index) in tags"
      :key="tag.name"
      size="medium"
      :closable="tag.name !== 'home'"
      :effect="$route.name === tag.name ? 'dark' : 'plain'"
      @click="changeMenu(tag)"
      @close="handleClose(tag, index)"
    >
      {{ tag.label+index }}
    </el-tag>
  </div>
</template>

<script>
import { mapState, mapMutations } from 'vuex'
export default {
  computed: { 
    ...mapState({
      tags: (state) => state.table.tabsList,
    }),
  },
  methods: {
    ...mapMutations({
      close: 'closeTag',
    }),
    //点击跳转到对应标签的页面
    changeMenu(item) {
      this.$router.push({ name: item.name })
    },
    //点击关闭对应标签
    handleClose(tag, index) {
      let length = this.tags.length - 1
      this.close(tag)
      //关闭的不是当前页,则不需要跳转,还停留在当前页
      if (tag.name !== this.$route.name) {
        return
      }
      //关闭的当前页 需要跳转展示列表
      // 当前页是最后一页,则需要跳转到它前一个
      //当前是不是最后一页,是中间页 则需要跳转到它后面一个
      if (index === length) {
        this.$router.push({
          name: this.tags[index - 1].name,
        })
      } else {
        
        this.$router.push({ name: this.tags[index].name })
      }
    },
  },
}
</script>
<style lang="scss" >
.tabs {
  padding: 10px;
   background: whitesmoke;
}
.mytag {
  float: left;
  margin-left: 15px;
  cursor: pointer;
 
}
</style>
五、补充辅助函数

mapState 、mapMutations 、mapActions 、mapGetters、mapActions。可以理解为包装的语法糖,使得我们在获取对应数据的时候能够一次获得多个(数组解构,简化代码),使用的方式也比较简单,参照上面使用格式就可以了。

欢迎分享,转载请注明来源:内存溢出

原文地址:https://www.54852.com/web/1297289.html

(0)
打赏 微信扫一扫微信扫一扫 支付宝扫一扫支付宝扫一扫
上一篇 2022-06-10
下一篇2022-06-10

发表评论

登录后才能评论

评论列表(0条)

    保存