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函数。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 单线程的Redis速度为什么快?
· SQL Server 2025 AI相关能力初探
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 展开说说关于C#中ORM框架的用法!