Vue3学习笔记 —— 状态管理、Vuex、Pinia (未完结)

发布时间 2023-03-22 21:16:18作者: __fairy

优秀文章分享:vue中使用vuex(超详细) - 掘金 (juejin.cn)

一、状态管理

1.1、什么是状态管理?

理论上来说,每一个 Vue 组件实例都已经在“管理”它自己的响应式状态了。我们以一个简单的计数器组件为例:


<!-- 视图 -->
<template>{{ count }}</template>
<script setup>
import { ref } from 'vue'

// 状态
const count = ref(0)

// 动作
function increment() {
  count.value++
}
</script>

它是一个独立的单元,由以下几个部分组成:

  • 状态:驱动整个应用的数据源;

  • 视图:对状态的一种声明式映射;

  • 交互:状态根据用户在视图中的输入而作出相应变更的可能方式。

“ 单向数据流 ” 概念的简单图示:

然而,当我们有多个组件共享一个共同的状态时,就没有这么简单了:

  1. 多个视图可能都依赖于同一份状态。

  2. 来自不同视图的交互也可能需要更改同一份状态。

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

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

一个更简单直接的解决方案是抽取出组件间的共享状态,放在一个全局单例中来管理。这样我们的组件树就变成了一个大的 “ 视图 ”,而任何位置上的组件都可以访问其中的状态或触发动作。

通过定义和隔离状态管理中的各种概念并通过强制规则维持视图和状态间的独立性,我们的代码将会变得更结构化且易维护。

这就是 Vuex(下文介绍) 背后的基本思想,借鉴了 FluxRedux 和 The Elm Architecture。与其他模式不同的是,Vuex 是专门为 Vue.js 设计的状态管理库,以利用 Vue.js 的细粒度数据响应机制来进行高效的状态更新。

1.2、用响应式 API 做简单状态管理

src / mystore.ts 文件:

// 用响应式 API 做简单状态管理
import {reactive} from 'vue';


// 把需要共享的数据存储在这里,然后导出
export const mystore=reactive({
    n:0,
    increment(i:any){
        this.n +=i;
    }
})

src / components / CounterA.vue 文件:

<template lang="">
    <div class="counter">
       <h2>计数器A - {{ mystore.n }}</h2> 
       <button @click="mystore.increment(1)">每次点击增加1</button>
    </div>
</template>
<script lang="ts" setup>
import { ref } from 'vue';
// 实现数据共享方法二:
// 导入ts文件
import {mystore} from '../mystore'

// 实现简单的计数器,但是两个组件的数据不是共享的
// 定义n=0
// let n = ref(0)
// function increment(i:number){
//     n.value +=i;
// }

</script>
<style scoped>
 .counter{
    background-color: #def;
 }   
</style>

src / components / CounterB.vue 文件:

<template lang="">
    <div class="counter">
       <h2>计数器B - {{ mystore.n }}</h2> 
       <button @click="mystore.increment(2)">每次点击增加2</button>
    </div>
</template>
<script lang="ts" setup>
import { ref } from 'vue';
// 实现数据共享方法二:
// 导入ts文件
import {mystore} from '../mystore'


// 实现简单的计数器,但是两个组件的数据不是共享的
// 定义n=0
// let n = ref(0)
// function increment(i:number){
//     n.value +=i;
// }

</script>
<style scoped>
 .counter{
    background-color: pink;
 }   
</style>

运行结果:

现在的需求:有一部分数据我想共享,有一部分数据我不想共享怎么办?

答:用 globalCount(全局状态) 和 localCount(局部状态)

src / mystore.ts 文件:

// 用globalCount和localCount做全局或局部状态管理
import {ref} from 'vue';

// 全局共享状态
let globalCount = ref(100);

// 把需要共享的数据存储在这里,然后导出
export function useStore(){
    // 局部共享状态,每个组件独享的
    let localCount = ref(200);


    return{
        globalCount,
        localCount,
        incrementLocal(){
            this.localCount.value++;
        },
        incrementGlobal(){
            this.globalCount.value++;
        }
    }
}

src / components / CounterA.vue 文件:

<template lang="">
    <div class="counter">
       <h2>计数器A - globalCount={{ store.globalCount }}localCount={{ store.localCount }}</h2> 
       <button @click="store.incrementGlobal(1)">全局增加1</button>
       <button @click="store.incrementLocal(1)">局部增加1</button>
    </div>
</template>
<script lang="ts" setup>
// 实现数据共享方法三:
// 导入ts文件
import {useStore} from '../mystore'

// 调用
const store = useStore();

</script>
<style scoped>
 .counter{
    background-color: #def;
 }   
</style>

src / components / CounterB.vue 文件:

<template lang="">
    <div class="counter">
       <h2>计数器B - globalCount={{ store.globalCount }}localCount={{ store.localCount }}</h2> 
       <button @click="store.incrementGlobal(1)">全局增加1</button>
       <button @click="store.incrementLocal(1)">局部增加1</button>
    </div>
</template>
<script lang="ts" setup>
// 实现数据共享方法三:
// 导入ts文件
import {useStore} from '../mystore'

// 调用
const store = useStore();

</script>
<style scoped>
 .counter{
    background-color: pink;
 }   
</style>

运行结果:

 以上的结果虽然可以实现数据共享的功能,但是这个只适合与小项目里面,对于复杂的项目就不适合了!

在vue中,我们想要实现父子组件中的传值,通过props和自定义事件可以可轻松的办到,如果是两个没有关联的组件,通过$bus事件公交也能实现兄弟子件的传值,但是在大型项目中,使用$bus容易导致代码繁琐,且不容易阅读.这个时候,vuex的出现可以很好的帮助我们解决我们这种问题

1.3、Pinia 与 VueX

虽然我们的手动状态管理解决方案在简单的场景中已经足够了,但是在大规模的生产应用中还有很多其他事项需要考虑:

  • 更强的团队协作约定

  • 与 Vue DevTools 集成,包括时间轴、组件内部审查和时间旅行调试

  • 模块热更新 (HMR)

  • 服务端渲染支持

Pinia 就是一个实现了上述需求的状态管理库,由 Vue 核心团队维护,对 Vue 2 和 Vue 3 都可用。

现有用户可能对 Vuex 更熟悉,它是 Vue 之前的官方状态管理库。由于 Pinia 在生态系统中能够承担相同的职责且能做得更好,因此 Vuex 现在处于维护模式。它仍然可以工作,但不再接受新的功能。对于新的应用,建议使用 Pinia。

二、Vuex

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

2.1、什么是“状态管理模式”?

让我们从一个简单的 Vue 计数应用开始:

const Counter = {
  // 状态
  data () {
    return {
      count: 0
    }
  },
  // 视图
  template: `
    <div>{{ count }}</div>
  `,
  // 操作
  methods: {
    increment () {
      this.count++
    }
  }
}

createApp(Counter).mount('#app')

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

  • 状态,驱动应用的数据源;

  • 视图,以声明方式将状态映射到视图;

  • 操作,响应在视图上的用户输入导致的状态变化。

成员列表:

  1. state: 统一定义管理公共数据

  2. mutations: 使用它来修改数据

  3. getters:类似于vue中的计算属性

  4. actions: 类似于methods,用于发起异步请求,比如axios

  5. modules:模块拆分

2.2、什么情况下我应该使用 Vuex

Vuex 可以帮助我们管理共享状态,并附带了更多的概念和框架。这需要对短期和长期效益进行权衡。

如果您不打算开发大型单页应用,使用 Vuex 可能是繁琐冗余的。确实是如此——如果您的应用够简单,您最好不要使用 Vuex。一个简单的 store 模式就足够您所需了。但是,如果您需要构建一个中大型单页应用,您很可能会考虑如何更好地在组件外部管理状态,Vuex 将会成为自然而然的选择。引用 Redux 的作者 Dan Abramov 的话说就是:

Flux 架构就像眼镜:您自会知道什么时候需要它。

2.3、第一个Vuex示例(计数器)

2.3.1、添加依赖

方法一:在脚手架 创建项目时勾选vuex的选项系统会自动创建

 方法二:npm  或 Yarn 安装

npm install vuex@next --save

yarn add vuex@next --save

2.3.2、定义存储对象

src / store / index.ts 文件:

// 定义存储对象

// 导入vuex
 import { createStore } from "vuex";


//  创建存储对象
export default createStore({
    // 状态,相当于data,数据
    state:{
        count:100
    },
    // 变更,相当于method,更新方法
    mutations:{
        // 参数一:状态,参数二:每次增长多少
        increment(state,n){
            state.count += n;
        }

    }
})

2.3.3、在main.ts 中挂载vuex

import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
// 导入vuex存储对象
import store from './store'

let app = createApp(App);

// 挂载路由中间件
app.use(router);

// 挂载插件
app.use(store);

app.mount('#app')

2.3.4、使用存储对象

src / components / CounterA.vue 文件:

<template lang="">
    <div class="counter">
       <h2>计数器A - {{store.state.count}}</h2> 
       <button @click="add(1)">增加1</button>
    </div>
</template>
<script lang="ts" setup>
// 实现数据共享方法四:
// 导入插件
import {useStore} from 'vuex'

// 调用
const store = useStore();

// 使用
function add(n:number){
    // 通过commit方法来调用mutations中的方法increment,指定参数n
    store.commit('increment',n);
}

</script>
<style scoped>
 .counter{
    background-color: #def;
 }   
</style>

src / components / CounterB.vue 文件:

<template lang="">
    <div class="counter">
       <h2>计数器B - {{store.state.count}}</h2> 
       <button @click="add(2)">增加2</button>
    </div>
</template>
<script lang="ts" setup>
// 实现数据共享方法四:
// 导入插件
import {useStore} from 'vuex'

// 调用
const store = useStore();

// 使用
function add(n:number){
    // 通过commit方法来调用mutations中的方法increment,指定参数n
    store.commit('increment',n);
}

</script>
<style scoped>
 .counter{
    background-color: pink;
 }   
</style>

运行结果:

2.4、state 数据

state:vuex的基本数据,用来存储变量

提供唯一的公共数据源,所有共享的数据统一放到store的state进行储存,相似与data。

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

2.4.1、定义state

// 定义存储对象

// 导入vuex
 import { createStore } from "vuex";


//  创建存储对象
export default createStore({
    // 状态,相当于data,数据
    state:{
        count:100,
        price: 95.7,
        user:{
            name:"tao",
            age: 18
        }
    },
    // 变更,相当于method,更新方法
    mutations:{
        // 参数一:状态,参数二:每次增长多少
        increment(state,n){
            state.count += n;
        }

    }
})

2.4.2、获取state方法一

组件内直接使用 $store 获取实例:

<template lang="">
    <div class="counter">
        <h2>计算器A</h2>
        <h3>
            价格:{{ $store.state.price }}
        </h3>
        <h3>
            数量:{{ $store.state.count }}
        </h3>
        <!-- 可以直接这样子使用,但是不建议,因为意义不大 -->
        <button @click="$store.state.count += 1">
      每次点击加1,当前值:{{ $store.state.count }}
    </button>
    </div>
</template>
<script lang="ts" setup>


</script>
<style scoped>
 .counter{
    background-color: #def;
 }   
</style>

2.4.3、获取state方法二

可以通过调用 useStore 函数,来在 setup 钩子函数中访问 store。这与在组件中使用选项式 API 访问 this.$store 是等效的。

<template lang="">
    <div class="counter">
        <h2>计算器A</h2>
        <h3>
            价格:{{ store.state.price }}
        </h3>
        <h3>
            数量:{{ store.state.count }}
        </h3>
        <!-- 可以直接这样子使用,但是不建议,因为意义不大 -->
        <button @click="$store.state.count += 1">
      每次点击加1,当前值:{{ $store.state.count }}
    </button>
    </div>
</template>
<script lang="ts" setup>
import { useStore } from 'vuex';
// 用这个变量代替$符号
const store = useStore();

</script>
<style scoped>
 .counter{
    background-color: #def;
 }   
</style>

2.4.4、获取state方法三

使用 this.$store 访问,如

// 请注意:这样子是拿不到this的
// const instance = getCurrentInstance();
// console.log("instance",instance);

我们需要取消语法糖的形式,然后使用计算属性定义一个计算的方法

<template lang="">
    <div class="counter">
        <h2>计算器A</h2>
        <h3>
            数量:{{count}}
        </h3>
        <h3>
            价格:{{price}}
        </h3>
    </div>
</template>
<script lang="ts">
export default {
    // 计算属性
    computed: {
        count(this: any) {
            return this.$store.state.count;
        }, 
        price(this: any) {
            return this.$store.state.price;
        }
    },

}



</script>
<style scoped>
.counter {
    background-color: #def;
}
</style>

运行结果:

请注意:直接在页面中使用 $store.state.count,结果非常麻烦,当一个组件需要获取多个状态的时候,将这些状态都声明为计算属性会有些重复和冗余。为了解决这个问题,我们可以使用 mapState 辅助函数帮助我们生成计算属性

2.4.4、mapState映射数据

<template lang="">
    <div class="counter">
        <h2>获取属性值</h2>
        <h3>
            数量:{{count}}
        </h3>
        <h3>
            价格:{{price}}
        </h3>
        <h3>
            用户:{{user}}
        </h3>
        
    </div>
</template>
<script lang="ts">
import { mapState } from 'vuex';

export default {
   // 用mapState映射数据
   computed:mapState(["count","price","user"])
       
}

</script>
<style scoped>
.counter {
    background-color: #def;
}
</style>

请注意:这里有一个缺点,万一自身有属性怎么办,栗子如下:

<template lang="">
    <div class="counter">
        <h3>
            消息:{{msg}}
        </h3>
        
    </div>
</template>
<script lang="ts">
import { mapState } from 'vuex';

export default {
    computed: {
        // 新属性
        msg() {
            return "这是一条信息"
        },
        // 展开
        ...mapState(["count", "price", "user"])
    }

}

</script>
<style scoped>
.counter {
    background-color: #def;
}
</style>

请注意:这里有一个新需求,把价格和数量组合在一起,变成一个新的数据怎么办,栗子如下:

方法一:箭头函数

<template lang="">
    <div class="counter">
        <h2>组合价格与数量的属性值:</h2>
        <h3>
            使用箭头函数:{{priceAndCount}}
        </h3>
        
    </div>
</template>
<script lang="ts">
import { mapState } from 'vuex';

export default {
    // 方法1、使用箭头函数
    computed: mapState({
        priceAndCount: state => state.price + "-" + state.count,
    })

}

</script>
<style scoped>
.counter {
    background-color: #def;
}
</style>

方法二:别名

<template lang="">
    <div class="counter">
        <h3>
            使用别名:{{mycount}}
        </h3>
        
    </div>
</template>
<script lang="ts">
import { mapState } from 'vuex';

export default {
    // 方法2:使用别名
    computed: mapState({
        priceAndCount: state => state.price + "-" + state.count,
        mycount: 'count',
    })

}

</script>
<style scoped>
.counter {
    background-color: #def;
}
</style>

方法三:函数

<template lang="">
    <div class="counter">
        <h3>
            使用函数:{{getPirce}}
        </h3>
        
    </div>
</template>
<script lang="ts">
import { mapState } from 'vuex';

export default {
    // 方法3:使用函数
    computed: mapState({
        priceAndCount: state => state.price + "-" + state.count,
        mycount: 'count',
        getPirce(state: any) {
            return state.price;
        }
    })

}

</script>
<style scoped>
.counter {
    background-color: #def;
}
</style>

运行结果:

2.5、getter 计算属性

getter:从基本数据(state)派生的数据,相当于state的计算属性

2.5.1、访问Getter的方法

store / index.ts 文件:

// 定义存储对象

// 导入vuex
 import { createStore } from "vuex";


//  创建存储对象
export default createStore({
    // 状态,相当于data,数据
    state:{
        count:100,
        price: 95.7,
        user:{
            name:"tao",
            age: 18
        }
    },
    // 相当于计算属性,对state的加工
    getters:{
        // 把两个属性组合
        countAndPirce(state){
            return state.price + "-" + state.count;
        }
    },
    // 变更,相当于method,更新方法
    mutations:{
        // 参数一:状态,参数二:每次增长多少
        increment(state,n){
            state.count += n;
        }

    }
})

方法一:通过属性访问

<template lang="">
    <div class="counter">
        <h1>访问Getter的方法</h1>
        <hr>
        <h2>方法一</h2>
        <h3>{{ $store.getters.countAndPirce }}</h3>
    </div>
</template>
<script lang="ts">

</script>
<style scoped>
.counter {
    background-color: #def;
}
</style>

方法二:通过方法访问

<template lang="">
    <div class="counter">
        <h1>访问Getter的方法</h1>
        <hr>
        <h2>方法二:方法</h2>
        <h3>{{ store.getters.countAndPirce }}</h3>
    </div>
</template>
<script lang="ts" setup>
import { useStore } from 'vuex';

const store = useStore();

</script>
<style scoped>
.counter {
    background-color: #def;
}
</style>

方法三:不用语法糖通过计算属性中的 this 访问

<template lang="">
    <div class="counter">
        <h1>访问Getter的方法</h1>
        <h2>方法三:计算属性</h2>
        <h3>{{ cAndp }}</h3>
    </div>
</template>
<script lang="ts" >
import { useStore } from 'vuex';
export default {
    setup(){
        const store = useStore();
        return {store}
    },

    // 用计算属性
    computed:{
        cAndp(this:any){
            return this.$store.getters.countAndPirce
        }
    }
}

</script>
<style scoped>
.counter {
    background-color: #def;
}
</style>

运行结果:

2.5.2、Getter 也可以调用另一个 getter 

store / index.ts 文件:

// 定义存储对象

// 导入vuex
import { createStore } from "vuex";

//  创建存储对象
export default createStore({
  // 状态,相当于data,数据
  state: {
    count: 100,
    price: 95.7,
    user: {
      name: "tao",
      age: 18,
    },
    todos: [
        { id: 1001, name: "健身", done: false },
        { id: 1002, name: "阅读", done: true },
        { id: 1003, name: "做饭", done: true } 
    ],
  },
  // 相当于计算属性,对state的加工
  getters: {
    // 把两个属性组合
    countAndPirce(state) {
      return state.price + "-" + state.count;
    },
    // 把todos数组中所有的数据拿出来
    todoDones(state){
        // 所有的任务
        return state.todos.filter(p=>p.done)
    },
    // 把todos数组中所有已经完成的数据拿出来
    todoDonesLength(state,getters){ // 参数二:返回已完成项的个数
        // 调用了上面的方法了
        return getters.todoDones.length;
    }
  },
  // 变更,相当于method,更新方法
  mutations: {
    // 参数一:状态,参数二:每次增长多少
    increment(state, n) {
      state.count += n;
    },
  },
});

使用:

<template lang="">
    <div class="counter">
        <h1>访问Getter的方法</h1>
        <hr>
        <h2>方法一:属性</h2>
        <h3>{{ $store.getters.countAndPirce }}</h3>
        <hr>
        <h2>方法二:方法</h2>
        <h3>{{ store.getters.countAndPirce }}</h3>
        <hr>
        <h2>方法三:计算属性</h2>
        <h3>{{ cAndp }}</h3>
        <hr>
        <h2>调用另一个getter</h2>
        <h3>todoDones:{{store.getters.todoDones}}</h3>
        <h3>getters.todoDones.length:{{store.getters.todoDones.length}}</h3>
    </div>
</template>
<script lang="ts" >
import { useStore } from 'vuex';
export default {
    setup(){
        const store = useStore();
        return {store}
    },

    // 用计算属性
    computed:{
        cAndp(this:any){
            return this.$store.getters.countAndPirce
        }
    }
}

</script>
<style scoped>
.counter {
    background-color: #def;
}
</style>

2.5.3、Vuex-getrs带参数与mapeGetters(辅助函数)

Vuex-getrs带参数:

定义:

// 函数里面返回函数
 findTask:(state)=>(id:any)=>{
     // 根据编号获得任务项
     return state.todos.find(p=>p.id==id)
 }

使用:

<h3>findTask:{{store.getters.findTask(1002)}}</h3>

mapeGetters(辅助函数):

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

把自己需要方法从index.ts文件里面映射过来,怎么办???

<h2>通过辅助函数映射方法过来</h2>
<h3>todoDones:{{todoDones}}</h3>
<h3>todoDonesLength:{{todoDonesLength}}</h3>

<script lang="ts" >
import { useStore, mapGetters } from 'vuex';

export default {
    setup(){
        const store = useStore();
        return {store}
    },

    // 用计算属性
    computed:{
        cAndp(this:any){
            return this.$store.getters.countAndPirce
        },

        // 把自己需要从index.ts文件里面的方法映射过来,怎么办???
        ...mapGetters(["todoDones","todoDonesLength"]),
    }
}

</script>

2.5、mutation 修改数据

 

2.6、actions 发起异步

2.7、modules 模块拆分