Vue学习—vuex

Vuex状态管理

1 什么是状态管理

​ 在开发中,我们会让应用程序需要处理各种各样的数据,这些数据需要保存在我们应用程序中的某一个位置,对于这些数据的管理我们就 称之为是状态管理

​ 在前面我们是如何管理自己的状态呢?

​ 在Vue开发中,我们使用组件化的开发方式:

  1. 在组件中我们定义data或者在setup中返回使用的数据,这些数 据我们称之为state;

  2. 在模块template中我们可以使用这些数据,模块最终会被渲染成DOM,我们称之为View;

  3. 在模块中我们会产生一些行为事件,处理这些行为事件时,有可能 会修改state,这些行为事件我们称之为actions;

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-TS1Imi1h-1668407537860)(assets/image-20221103054848920.png)]

2 复杂的状态管理

​ JavaScript开发的应用程序,已经变得越来越复杂了.JavaScript需要管理的状态越来越多,越来越复杂:

  • 这些状态包括服务器返回的数据、缓存数据、用户操作产生的数据等等;
  • 也包括一些UI的状态,比如某些元素是否被选中,是否显示加载动效,当前分页;

然而,当我们有多个组件共享一个共同的状态时,就没有这么简单了:单向数据流的简洁性很容易被破坏:

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

对于情景 1,一个可行的办法是将共享状态“提升”到共同的祖先组件上去,再通过 props 传递下来。然而在深层次的组件树结构中这么做的话,很快就会使得代码变得繁琐冗长。这会导致另一个问题:Prop 逐级透传问题

Prop 逐级透传问题:

​ 通常情况下,当我们需要从父组件向子组件传递数据时,会使用 props。想象一下这样的结构:有一些多层级嵌套的组件,形成了一颗巨大的组件树,而某个深层的子组件需要一个较远的祖先组件中的部分数据。在这种情况下,如果仅使用 props 则必须将其沿着组件链逐级传递下去,这会非常麻烦:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-KG1JAzsp-1668407537862)(assets/image-20221103053130854.png)]

注意,虽然这里的 <Footer> 组件可能根本不关心这些 props,但为了使 <DeepChild> 能访问到它们,仍然需要定义并向下传递。如果组件链路非常长,可能会影响到更多这条路上的组件。这一问题被称为“prop 逐级透传”,显然是我们希望尽量避免的情况。

对于情景 2,我们经常发现自己会直接通过模板引用获取父/子实例,或者通过触发的事件尝试改变和同步多个状态的副本。但这些模式的健壮性都不甚理想,很容易就会导致代码难以维护。

而且管理不断变化的state本身是非常困难的:

  • 状态之间相互会存在依赖,一个状态的变化会引起另一个状态的变化,View页面也有可能会引起状态的变化;
  • 当应用程序复杂时,state在什么时候,因为什么原因而发生了变化,发生了怎么样的变化,会变得非常难以控制和追踪;

​ 一个更简单直接的解决方案是抽取出组件间的共享状态,放在一个全局单例中来管理。这样我们的组件树就变成了一个大的“视图”,而任何位置上的组件都可以访问其中的状态或触发动作。通过定义和隔离状态管理中的各个概念,并通过强制性的规则来维护视图和状态间的独立性,我们的代码边会变得更加结构化和易于维护、跟踪;这就是Vuex背后的基本思想.

3 Vuex

​ Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式 + 库。它采用集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-qL4MfW06-1668407537863)(assets/image-20221105093013001.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-cHju7cR9-1668407537865)(assets/image-20221103053957771.png)]

每一个 Vuex 应用的核心就是 store,里面又包括:
(1)state(数据):用来存放数据源,就是公共状态;
(2)getters(数据加工):有的时候需要对数据源进行加工,返回需要的数据;
(3)actions(事件):要执行的操作,可以进行同步或者异步事件
(4)mutations(执行):操作结束之后,actions通过commit更新state数据源
(5)modules:使用单一状态树,致使应用的全部状态集中到一个很大的对象,所以把每个模块的局部状态分装使每一个模块拥有本身的 state、mutation、action、getters、甚至是嵌套子模块;

vuex的工作流程就是:
(1)通过dispatch去提交一个actions,
(2) actions接收到这个事件之后,在actions中可以执行一些异步|同步操作,根据不同的情况去分发给不同的mutations,
(3)actions通过commit去触发mutations,
(4)mutations去更新state数据,state更新之后,就会通知vue进行渲染。

4.Vuex的使用

第一步: 安装vuex

npm install vuex

第二步: 在src目录下创建一个store目录,在该目录下创建index.js文件,用于创建Store对象

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-vrZeYtHX-1668407537867)(assets/image-20221105112705487.png)]

//用于创建Vuex的核心对象Store
//1.导入
import {createStore } from 'vuex'


//2.创建Store对象
const store = createStore({
   //state 用于存储数据
   state(){
      return {
         count:1,
      }
   },
   //actions 用于响应组件中的事件
   actions:{

   },
   //mutations 用于操作数据
   mutations:{

   }
});


//3.暴露出store对象
export default store;

第三步: 在main.js 使用store对象

import store from './store';
app.use(store);

第四步: 在 Vue 组件中, 可以通过 this.$store 访问store实例

<template>
   <h2>{{$store.state.count}}</h2>
</template>

<script>
export default {
 name: 'App',
 data() {
   return {
     
   };
 },
 components: {
 },
 methods: {
  
 },
 mounted() {
   console.log(this.$store.state.count);
 }
};
</script>

<style lang="css" scoped>
</style>

接下来,我们使用一个案例,熟悉vuex的基本使用:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-WSLsKQqR-1668407537868)(assets/1.gif)]

CalcSum.vue

<template>
    <div>
        <h2>和:{{$store.state.sum}}</h2>
        <input type="number" length="3" v-model="num"/>
        <input type="button" value="+" @click="add"/>
        <input type="button" value="-" @click="sub"/>
    </div>
</template>

<script>
export default {
    name: 'CalcSum',

    data() {
        return {
            num:1,
        };
    },

    mounted() {
        
    },

    methods: {
        add(){
            //通过dispatch去提交一个action
            this.$store.dispatch('increment', this.num);
        },
        sub(){
            this.$store.dispatch('decrement', this.num);
        },
    },
};
</script>

<style lang="css" scoped>

</style>

App.vue

<template>
    <CalcSum/>
</template>

<script>
import CalcSum from './components/CalcSum.vue'
export default {
  name: 'App',
  data() {
    return {
       num:1,
    };
  },
  components: {
    CalcSum
  },
  methods: {
   
  },
  mounted() {
    console.log(this.$store.state.sum);
  }
};
</script>

<style lang="css" scoped>
</style>

store/index.js

//用于创建Vuex的核心对象Store
//1.导入
import {createStore } from 'vuex'

//2.创建Store对象
const store = createStore({
    //state 用于存储数据
    state(){
       return {
          sum:0,
       }
    },
    //actions 用于响应组件中的事件
    actions:{
        increment:function(context,value){
            //actions通过commit去触发mutations
            context.commit("INCREMENT",value)
        },
        decrement(context,value){
            context.commit("DECREMENT",value)
        }

    },
    //mutations 用于操作数据
    mutations:{
        //mutations去更新state数据
        INCREMENT(store,value){
            store.sum +=value;   
        },
        DECREMENT(store,value){
            store.sum -= value; 
        }
    }
});


//3.暴露出store对象
export default store;

注意:

每次我们在组件获取vuex的状态数据需要写{{$store.state.属性名}},太复杂了,由于 store 中的状态是响应式的,在组件中调用 store 中的状态简单到仅需要在计算属性中返回即可。

computed: {
   count () {
       return this.$store.state.sum
   }
}
{{count}}

5. 核心概念

5.1 单一状态树

​ Vuex 使用单一状态树——是的,用一个对象就包含了全部的应用层级状态。至此它便作为一个“唯一数据源 (SSOT)”而存在。这也意味着,每个应用将仅仅包含一个 store 实例。单一状态树让我们能够直接地定位任一特定的状态片段,在调试的过程中也能轻易地取得整个当前应用状态的快照。

单一状态树的优势:

  • 如果你的状态信息是保存到多个Store对象中的,那么之后的管理和维护等等都会变得特别困难;
  • 所以Vuex也使用了单一状态树来管理应用层级的全部状态;
  • 单一状态树能够让我们最直接的方式找到某个状态的片段,而且在之后的维护和调试过程中,也可以非常方便的管理和维护;

注意:

  使用 Vuex 并不意味着你需要将**所有的**状态放入 Vuex。虽然将所有的状态放到 Vuex 会使状态变化更显式和易调试,但也会使代码变得冗长和不直观。如果有些状态严格属于单个组件,最好还是作为组件的局部状态。你应该根据你的应用开发需要进行权衡和确定。
5.2 mapState辅助函数

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

​ 比如我们在vuex的state中存储了多个数据,在某个组件我们需要获取到这些数据,这时就会看到如下代码:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-gSXVJTB8-1668407537869)(assets/image-20221107060923278.png)]

我们发现$store.state 一直在重复写,那我们可以把这些设置为计算属性:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-nqNUhTiF-1668407537870)(assets/image-20221107061214983.png)]

但是我们还是发现计算属性中,也在重复写this.$store.state。vuex提供了mapState 辅助函数帮助我们生成计算属性.

 computed:mapState({
        
        //箭头函数可使代码更简练
        name: state => state.name,
        // 传字符串参数 'name' 等同于 `state => state.name`
        //给name定义一个别名
        xingming: 'name',
        //扩展函数,可以对name进行操作
        namePlusLocalState (state) {
            return this.hello + state.name;
        },
        sex:state=>state.sex,
        age:state=>state.age,
    }),

当映射的计算属性的名称与 state 的子节点名称相同时,我们也可以给 mapState 传一个字符串数组

 computed:mapState(["name","sex","age"]),

mapState 函数返回的是一个对象。我们如何将它与局部计算属性混合使用呢?通常,我们需要使用一个工具函数将多个对象合并为一个,以使我们可以将最终对象传给 computed 属性。但是自从有了对象展开运算符,我们可以极大地简化写法:

//对象展开运算符
computed:{
    //局部计算属性
    localComputed () { /* ... */ },
    ...mapState(["name","sex","age"]),
},
5.3 Getter

某些属性我们可能需要经过变化后来使用,这个时候可以使用getters:Vuex 允许我们在 store 中定义“getters”(可以认为是 store 的计算属性)

上图我们中函数Getter 接受 state 作为其第一个参数:

 getters:{
        totalPrice(state){
            let total= 0;
            state.books.forEach((book)=>{
                total += book.count*book.price;
            })

            return total;
        }
    },

我们在其他组件可以访问Getter 对象,Getter 会暴露为 store.getters 对象,你可以以属性的形式访问这些值:

 <div>
     <h2>总价:{{$store.getters.totalPrice}}</h2>
</div>

getters可以接收第二个参数

 getters:{
        totalPrice(state,getters){
            let total= 0;
            state.books.forEach((book)=>{
                total += book.count*book.price;
            })

            return total+","+getters.myName;
        },
        myName(state){
            return state.name;
        }
    },

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

 getters:{
        totalPrice(state,getters){
            let total= 0;
            state.books.forEach((book)=>{
                total += book.count*book.price;
            })

            return total+","+getters.myName;
        },
        myName(state){
            return state.name;
        },
        getBookById(state){
            return (id)=>{
                return state.books.find(book=>
                    book.id === id
                );
            }
        }
    },

访问:

<h2>书籍信息:{{$store.getters.getBookById(3)}}</h2>
5.4 mapGetters 辅助函数

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

<template>
    <div>
        <h2>总价:{{totalPrice}}</h2>
        <h2>书籍信息:{{getBookById(3)}}</h2>
    </div>
</template>

<script>
import { mapGetters } from 'vuex'
export default {
    name: 'Book',
    computed:{
        ...mapGetters(['totalPrice','getBookById']),
    },
};
</script>

<style lang="scss" scoped>

</style>

5.5 Mutation

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

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-qwD7qtlY-1668407537873)(assets/image-20221107115148803.png)]

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

在大多数情况下,载荷应该是一个对象,这样可以包含多个字段并且记录的 mutation 会更易读:

 increment:function(context,value){
            context.commit("INCREMENT",{
                value
            });
        },

INCREMENT(store,payload){
            store.sum +=payload.value;   
        },

提交 mutation 的另一种方式是直接使用包含 type 属性的对象:

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

当使用对象风格的提交方式,整个对象都作为载荷传给 mutation 函数,因此处理函数保持不变:

5.5.1 在组件中提交 Mutation

​ 你可以在组件中使用 this.$store.commit('xxx') 提交 mutation,或者使用 mapMutations 辅助函数将组件中的 methods 映射为 store.commit 调用(需要在根节点注入 store

<template>
    <div class="box">
        <h2>和:{{$store.state.sum}} --{{count}}</h2>
        <input type="number" length="3" v-model="num"/>
        <input type="button" value="+" @click="add({value:num})"/>
        <input type="button" value="-" @click="sub({value:num})"/>
        <Counter/>
    </div>
</template>

<script>
import {mapMutations } from 'vuex'
export default {
    name: 'CalcSum',

    data() {
        return {
            num:1,
        };
    },

    mounted() {
        
    },
    methods: {
        /*
        add(){
            //通过dispatch去提交一个action
            this.$store.dispatch('increment', this.num);
            console.log(this.$store.state.sum)
            //强制刷新页面
            //this.$forceUpdate();
        },
        sub(){
            this.$store.dispatch('decrement', this.num);
            //this.$forceUpdate();
        },*/
        
        ...mapMutations({
            add:'INCREMENT',
            sub:'DECREMENT'
        }),
    },
    computed: {
        count () {
             return this.$store.state.sum
        }
    }
};
</script>

<style lang="css" scoped>
    .box{
        width:70%;
        margin: auto;
    }
</style>

如果组件的事件函数名与Mutation中修改状态的函数名一样,我们可以简写:

...mapMutations(['INCREMENT','DECREMENT']),

mutation重要原则:

​ 一条重要的原则就是要记住 mutation 必须是同步函数,这是因为devtool工具会记录mutation的日记;每一条mutation被记录,devtools都需要捕捉到前一状态和后一状态的快照;但是在mutation中执行异步操作,就无法追踪到数据的变化;
​ 所以Vuex的重要原则中要求 mutation必须是同步函数;

5.6 Action

​ 如果我们希望在Vuex中发送网络请求的话需要如何操作呢?那我们就使用Action

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。当我们在之后介绍到 Modules 时,你就知道 context 对象为什么不是 store 实例本身了。

5.6.1 actions的分发操作

如何使用action呢?进行action的分发:

  • 分发使用的是 store 上的dispatch函数;

  • 同样的,它也可以携带我们的参数。

  • 也可以以对象的形式进行分发。

乍一眼看上去感觉多此一举,我们直接分发 mutation 岂不更方便?实际上并非如此,还记得 mutation 必须同步执行这个限制么?Action 就不受约束!我们可以在 action 内部执行异步操作:

5.6.2 actions的辅助函数

action也有对应的辅助函数:使用 mapActions 辅助函数将组件的 methods 映射为 store.dispatch 调用(需要先在根节点注入 store

ttps://vuex.vuejs.org/zh/guide/modules.html) 时,你就知道 context 对象为什么不是 store 实例本身了。

5.6.1 actions的分发操作

如何使用action呢?进行action的分发:

  • 分发使用的是 store 上的dispatch函数;
    在这里插入图片描述

  • 同样的,它也可以携带我们的参数:
    [外链图片转存中...(img-Rx8YthXG-1668407537878)]

  • 也可以以对象的形式进行分发:
    [外链图片转存中...(img-fRw5Veuk-1668407537880)]

乍一眼看上去感觉多此一举,我们直接分发 mutation 岂不更方便?实际上并非如此,还记得 mutation 必须同步执行这个限制么?Action 就不受约束!我们可以在 action 内部执行异步操作:

5.6.2 actions的辅助函数

action也有对应的辅助函数:使用 mapActions 辅助函数将组件的 methods 映射为 store.dispatch 调用(需要先在根节点注入 store在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值