Blazor和Vue对比学习(进阶.状态管理-2):状态共享,Vue的Pinia

前面章节,我们学过了父传子、子传父、祖传孙等,这些方法都是在组件树中进行数据的传递和管理,如果要在兄弟之间,或者远亲关系的组件之间进行数据的传递,利用之前的知识点,会是一件相当棘手的事情。

这个时候,如果我们跳出组件树,独立于组件树之外,创建一个用来存储数据的对象(称它为存储库),组件树中的所有组件都可以从这个存储库中,读取数据、写入数据和调用方法,过程就会简化很多。

这个存储库类似于一个全局对象,但它比全局对象更高级,体现在跨组件/页面之间共享状态,重点在“共享”。具体来说,它是响应式的,如果我们在一个组件中,更新了存储库中的某个数据,其它引用这个数据的组件,也会响应式的更新。

由于这块的内容比较长,我们分两篇来学习,首先是Vue的Pinia

 

Vue中,我们使用Vuex或Pinia来实现这个存储库,Pinia是Composition API出现后,新一代的状态管理插件,官方现在也推荐使用Pinia。如何安装和引入,可查询官方文档,最后一个小节会有简要说明。我们还是主要学习一下如何使用。

一、概述

先上一段代码,通过这段代码,我们尝试理解一个Pinia的本质

复制代码
//写义一个存储库
import { defineStore } from 'pinia'
export const useCounterStore = defineStore('counter', {
  //定义状态state
  state: () => {
    return { count: 0 };
  },
  //定义计算属性getters
  getters: {
    squareCount(state){
      return state.count*state.count
    }
  },
  //定义方法actions,可以是同步,也可以是异步
  actions: {
    increment() {
      this.count++;
    }
  }
})


//在组件中使用
import { useCounterStore } from '@/stores/counter';
export default {
  setup() {
    //获取存储库
    const counter = useCounterStore();
    //修改状态-自增
    counter.count++;
    //获取计算属性
    const squareCount = counter.squareCount;
    //调用存储库的方法
    counter.increment();
  },
}
复制代码

 

 1、在存储库中:

  • import { defineStore } from 'pinia'
    引入插件pinia给我们提供的defineStore对象,它是一个方法。defineStore方法,接受两个参数,一是字符串类型的ID,用于框架内部识别不同的存储库;二是一个对象,在这个对象中,我们定义字段/state、计算属性/getters和方法/actions(案例中的写法还是传统的选项式,实际上还有一个函数式的写法)。这个方法的返回值,是一个存储库方法(用于调用存储库),所以用use前缀。
  • export const useCounterStore = defineStore(“ID”,{})
    调用defineStore()方法,返回存储库方法,并export出去,存储库方法useCounterStore,还不是真正的存储库,在组件中调用它之后,才是真正返回一个存储库对象/store

2、在组件中:

  • import { useCounterStore } from '@/stores/counter'
    ...const counter = useCounterStore()
    在组件中,引入存储库方法并调用,真正返回一个存储库对象counter。需要注意的是,存储库方法返回的counter是一个reactive对象。
  • counter.count++
    counter.increment()
    因为存储库counter是一个reactive对象,所以可以直接调用存储库的数据/state和方法/action。存储库以及组件树中其它引用存储库的组件,会响应式更新。

3、另外一种定义存储库的方式:

  • export const useCounterStore = defineStore('counter', () => {
        const count = ref(0)
        function increment() {
        count.value++
    }
    return { count, increment }})
    这种方式是完全的Composition API了,但因为没有setup语法糖,所以记得要将定义好的state、getters和actions,return出去。官方文档都使用选项式,但我们后面使用的案例,都使用函数式。

4、存储库对象的成员

  • 存储库对象的成员,包括state/字段、getters/计算属性、actions/方法,和组件逻辑层的成员差不多

 

 

二、存储库对象成员的定义和使用

1、状态state

1)在Store中定义:有两种方式,一是选项式,二是函数式(推荐)。使用函数式时,定义的数据必须是响应式的,如《Blazor和Vue对比学习(基础1.2):模板语法和Razor语法》中对ref和reactive的对比,无论是基础类型还是数组类型,都建议使用ref。

 

2)在组件中使用:

①无论是选项式,还是函数式,在组件中获取存储库时,都是一个reactive对象,通过点运算符,可以直接获取存储库的成员。如果想通过解构方式直接获得存储库的成员,可使用以下方式解构:【const { count,squareCount,increment } = storeToRefs(useCounterStore())】,这样解构出来的成员才是响应式的,直接解构会失去响应式

②修改状态,有以下几种方式:

  • A,直接修改,如【counter.count++】;
  • B,使用API($pacth对象式),如【counter.$pacth({ count: counter.count+1 })】;
  • C,使用API($pacth函数式),如【counter.$pacth( (state)=>{ state.count++ } )】。如果使用$pacth,推荐使用函数式,有利于操作数组对象

③订阅状态,通过订阅状态,当状态改变时,可以执行一个回调函数,类似于watch。如【counter.$subscribe( (mutation, state)=>{} )。回调参数说明:

  • mutation,这个参数可以获到状态改变的类型(mutation.type),存储库ID(mutation.storeId)等
  • 变更后的状态state。通过订阅,我们可以将状态数据进行持久化操作,如只要有变化,就保存到locallStorage,【localStorage.setItem('cart', JSON.stringify(state))】

④其它API

  • A,重置状态,如【counter.$reset()】,状态数据变为定义中的初始值。注意:如果使用函数式定义存储库,则此API无法使用;
  • B,替换整个状态,如【counter.$state{ counter:15,name:"MC" }】

 

2、计算属性getters

1)选项式:即可以在参数中传入state,也可以直接在方法体中调用this,如【getters: { squareCount(state){return state.count*state.count;} }】,等价于【getters: { squareCount(){return this.count*this.count;} }】

2)函数式:和在setup中写计算属性一样,const squareCount = computed( ()=>{ return count*count;} )

3)可以给getter传递参数吗?可以的,return一个回调函数就可以实现。但不建议这么用。

         定义:return一个回调函数,const squareCount = computed( ()=>{ return (num)=>{ return count*count*num; };} )

         使用:像调用方法一样,使用计算属性,squareCount(2)

 

3、方法 actions

1)和methods的使用没有什么不同,比如在选项式中,通过this获得state,在函数式中可以直接使用定义的响应式数据变量

2)订阅方法:可以订阅action,在action执行的前后做一些事情。counter.$onAction( ( { store,name,args,after,onError } )=>{ ......after(result)=>{} ......onError(error)=>{}...... } )。after和onError,其实就是promise的回调。

 

4、关于状态订阅和方法订阅的补充

(1)状态订阅和方法订阅,都是绑定在组件中的,组件销毁后,订阅自动取消。如果想在组件销毁后,仍保持订阅,可以在订阅的回调中传入第二个参加,如【counter.$subscribe( callback,true )】

(2)手动取消订阅的方式:实际上调用订阅函数时,会返回一个函数,调用这个函数,就可以取消订阅。如【 const unAction = counter.$onAction(callback) 】> 【 unAction() 】

 

三、案例:此案例为简单的购物车应用,其中关于使用浏览器的locallStorage进行持久化保存的知识点,详见《状态管理》的第4节。

复制代码
//定义存储库cart.ts
import { defineStore } from "pinia";
import { computed, Ref, ref } from "vue";

export const useCartStore = defineStore("cart",()=>{

    //定义产品类型,TS类型约束
    type TProduct = {
        name:string,
        price:number,
        num:number
    }

    //购物车产品,使用REF<>类型约束
    const products:Ref<TProduct[]> = ref([
        {
            name:"iphone13",
            price:7000,
            num:1
        },
        {
            name:"Mate10",
            price:5000,
            num:1
        }
    ]);

    //计算属性getters。计算总金额
    const amount = computed(()=>{
        return products.value.reduce((p,c)=>{
            return p + c.num*c.price;
        },0)
    });

    //购物车加减数量actions。方法的参数为当前行索引
    function incrNum(index:number){
        products.value[index].num++;
    }
    function decrNum(index:number){
        products.value[index].num--;
    }

    return { products,amount,incrNum,decrNum };
})



//在组件中使用存储库
<template>
  <h1>总金额: {{cart.amount}}</h1>
  <h3 v-for="(item,index) in cart.products" :key="item.name">
    品名:{{item.name}};单价:{{item.price}};
    <button @click="cart.decrNum(index)">-</button>
    {{item.num}}
    <button @click="cart.incrNum(index)">+</button>
  </h3>
  <h3><button @click="reset()">复原</button></h3>
</template>


<script setup lang="ts">
import { storeToRefs } from "pinia";
import { onMounted } from "vue";
import { useCartStore } from "./stores/cart";

//获取存储库cart
const cart = useCartStore();

//以下解构incrNum和decrNum,报错,不知道什么原因?state和getters都没问题
//const { products,amount,incrNum,decrNum } = storeToRefs(useCartStore());

//订阅state,当state更新时,将存储库保存到浏览器的locallStorage,持久化保存
cart.$subscribe((mutation, state)=>{
  localStorage.setItem("cart",JSON.stringify(state));
})

//生命周期钩子加载时,判断浏览器中的存储库是否为空,如不为空,则将存储库替换为浏览器中的存储库
onMounted(()=>{
  //获取storage中的购物车数据
  const cartStorage = localStorage.getItem("cart");
  //如果storage中有购物车数据,则将state更新为storage中的数据
  console.log(cartStorage);
  if(cartStorage){
    cart.$state = JSON.parse(cartStorage);
  }
})

//复原购物车中的初始数据
//报错using the setup syntax and does not implement $reset()
//如果存储库定义,使用函数式,则$reset()无法使用
function reset(){
  cart.$reset();
}

</script>
复制代码

 

 

四、通过Pinia的两种扩展用法,领会Composition API

1、在一个Store中使用其它Store

和在组件中使用一样,第一步,引入存储库方法:import { useCounterStore } from '@/stores/counter'; 第二步,获得存储库:const counter = useCounterStore()......第三步,使用存储库。

 

2、扩展Store的成员

(1)现在,我们回到Pinia的安装:

第一步,安装npm包,【npm install pinia】

第二步,在入口文件main中,定义一个根存储,然后以插件方式使用它

复制代码
import { createApp } from 'vue'  //引入vue提供的createApp方法,这个方法创建根组件

import App from './App.vue'  //引入根组件的模板

import { createPinia} from 'pinia' //引入pinia提供的createPinia方法,这个方法创建根存储

const pinia = createPinia() //创建根存储对象

const app = createApp(App)  //创建根组件对象

app.use(pinia).mount('#app')  
//①将根组件对象挂载到入口的html文档中(index.html);
//②以插件方式使用根存储对象,上文中创建的独立的Store,都挂载到这个根存储下(继承)
//与组件不同之处在于,组件是以树的形式。而存储库之间的关系是平行的,与根存储之间,则是继续关系
复制代码

 

(2)如何扩展Store的成员?

默认情况下,存储库只有state、getters、actions三个成员,我们可以在创建根存储时,为根存储增加属性,那么所有存储库对象,都会继承这个属性。如下所示:

复制代码
import { createApp } from 'vue' 
import App from './App.vue'
import { createPinia} from 'pinia' 
const pinia = createPinia() 

//通过pinia.use(()=>{})形式,扩展
function secret(){
  return {secret:"this is a secret"}
}
pinia.use(secret)
const app = createApp(App) 

//其组件中,所有存储库都可以使用这个对象
const counter = useCounterStore();
const secret = counter.secret
复制代码

 

2、Composition API

可以看出,Vue引入Composition API后,可以很容易实现函数之间相互调用,从而实现逻辑复用,比使用各种包或库更加方便。甚至现在有一个专门的VueUse社区,提供各种方便的轮子,可以浏览:https://vueuse.org,已经有超过200多个方便的use函数。

 

posted @   functionMC  阅读(401)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 单线程的Redis速度为什么快?
· SQL Server 2025 AI相关能力初探
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 展开说说关于C#中ORM框架的用法!
点击右上角即可分享
微信分享提示