写在前面:本文参考小满大牛的pinia专栏
一、Vuex与Pinia
Vuex 和 Pinia 均是 Vue.js 的状态管理库,它们为 Vue 应用程序提供了一种集中式的、可预测的状态管理解决方案。
Vuex 是 Vue.js 官方推荐的状态管理库之一。它的核心概念包括 state、mutation、action 和 getter。其中,state 代表应用程序的状态数据,在 Vuex 中存储为唯一的来源,mutation 用于修改状态数据并确保数据变化的可追踪性,action 用于处理异步操作或组合多个 mutation 操作,getter 可以让我们对 state 进行计算和派生,并使其变得更加易于访问。一个 Vuex store 实例是一个全局 JavaScript 对象,可以在所有组件中通过注入来进行访问和操作。
Pinia 是一个新的状态管理库,也是专门为 Vue 3 开发的。它提供了一个类似于 Vuex 的状态管理模式,但采用最新的 Vue 3 API 构建。相比 Vuex,Pinia 更简单、更轻量,更加灵活,并支持 TypeScript 类型检查。
与 Vuex 相比,Pinia 没有严格的命名约定,可以自由拆分逻辑、支持独立实例、执行更轻量级的代码等。但不同于 Vuex,它并没有内置支持模块化和严格调试工具。
以下是Pinia的特点:
- 完整的 ts 的支持;
- 足够轻量,压缩后的体积只有1kb左右;
- 去除 mutations,只有 state,getters,actions;
- actions 支持同步和异步;
- 代码扁平化没有模块嵌套,只有 store 的概念,store 之间可以自由使用,每一个store都是独立的
- 无需手动添加 store,store 一旦创建便会自动添加;
- 支持Vue3 和 Vue2
Pinia官方文档
二、安装、引入Pinia
安装:npm install pinia
或 yarn add pinia
引入注册:
Vue3的引入方法
main.ts
import { createApp } from 'vue'
import App from './App.vue'
// Vue3的引入方法
import {createPinia} from 'pinia'
const store = createPinia()
let app = createApp(App)
app.use(store)
app.mount('#app')
Vue2的引入方法
main.ts
import { createPinia, PiniaVuePlugin } from 'pinia'
Vue.use(PiniaVuePlugin)
const pinia = createPinia()
new Vue({
el: '#app',
pinia,
})
三、初始化创建Store
新建文件夹
index.ts
用于管理仓库,store-name.ts
用于存储所有枚举的仓库名。
store-name.ts
// 枚举所有仓库名并暴露
export const enum Names {
TEST = 'TEST'
}
index.ts
// 导入定义仓库的方法
import { defineStore } from "pinia";
// 导入枚举的所有仓库名
import { Names } from "./store-name";
// defineStore()定义一个仓库,第一个参数作为名称,也可看作是id
// 这个id(名称)是必要的,Pinia使用它来讲store连接DevTools,可以通过调试工具查看
export const useTestStore = defineStore(Names.TEST, {
state: () => {
return {
// 定义初始化的值
current: 1,
name: '小5'
}
},
// 类似于computed可以帮我们修饰我们的值
getters: {
},
// 可以操作异步 和 同步 提交state
actions: {
}
})
简单的使用仓库的数据
App.vue
<template>
<div>pinia-current:{{ Test.current }}</div>
<div>pinia-name:{{ Test.name }}</div>
</template>
<script setup lang="ts">
import { useTestStore } from "./store";
const Test = useTestStore();
</script>
<style lang="scss" scoped></style>
结果展示:
四、修改State值的五种方式
1.直接修改
State是允许不在仓库中直接修改值的,这与Vuex不同,Vuex要通过commit
或dispatch
方法调用仓库修改。
<template>
<div>pinia-current:{{ Test.current }}</div>
<div>pinia-name:{{ Test.name }}</div>
<div style="display: flex; flex-direction: column">
<button @click="change">change</button>
</div>
</template>
<script setup lang="ts">
import { useTestStore } from "./store";
const Test = useTestStore();
const change = () => {
Test.current++;
};
</script>
<style lang="scss" scoped></style>
2.批量修改
在仓库的实例上有$patch方法可以批量修改多个值
<template>
<div>pinia-current:{{ Test.current }}</div>
<div>pinia-name:{{ Test.name }}</div>
<div style="display: flex; flex-direction: column">
<button @click="bulkChange">批量change</button>
</div>
</template>
<script setup lang="ts">
import { useTestStore } from "./store";
const Test = useTestStore();
const bulkChange = () => {
Test.$patch({
current: 888,
name: "小4",
});
};
</script>
<style lang="scss" scoped></style>
3.批量修改工厂函数形式
推荐使用函数形式 可以自定义修改逻辑
<template>
<div>pinia-current:{{ Test.current }}</div>
<div>pinia-name:{{ Test.name }}</div>
<div style="display: flex; flex-direction: column">
<button @click="funChange">工厂函数实现批量change</button>
</template>
<script setup lang="ts">
import { useTestStore } from "./store";
const Test = useTestStore();
const funChange = () => {
Test.$patch((state) => {
(state.current = 999), (state.name = "小3");
});
};
</script>
<style lang="scss" scoped></style>
4.通过原始对象修改整个实例
$state您可以通过将store的属性设置为新对象来替换store的整个状态
缺点就是必须修改整个对象的所有属性,可以用结构赋值的方式解决这个缺点。
<template>
<div>pinia-current:{{ Test.current }}</div>
<div>pinia-name:{{ Test.name }}</div>
<div style="display: flex; flex-direction: column">
<button @click="allChange">必须修改全部的写法(不推荐写法)</button>
</div>
</template>
<script setup lang="ts">
import { useTestStore } from "./store";
const Test = useTestStore();
const allChange = () => {
Test.$state = {
...Test.$state,
name: "小2",
};
};
</script>
<style lang="scss" scoped></style>
5.通过actions修改
在仓库中定义Actions
在仓库的actions 中直接使用this就可以指到state里面的值
store/index.ts
import { defineStore } from "pinia";
import { Names } from "./store-name";
export const useTestStore = defineStore(Names.TEST, {
state: () => {
return {
current: 1,
name: '小5'
}
},
getters: {
},
// 可以操作异步 和 同步 提交state
actions: {
// 不能写箭头函数 否则this会指向错误
setCurrent(num:number) {
this.current = num
}
}
})
直接在App.vue
实例中调用
<template>
<div>pinia-current:{{ Test.current }}</div>
<div>pinia-name:{{ Test.name }}</div>
<div style="display: flex; flex-direction: column">
<button @click="actionsChange">使用actions修改</button>
</div>
</template>
<script setup lang="ts">
import { useTestStore } from "./store";
const Test = useTestStore();
const actionsChange = () => {
Test.setCurrent(555);
};
</script>
<style lang="scss" scoped></style>
6.结果展示
App.vue
<template>
<div>pinia-current:{{ Test.current }}</div>
<div>pinia-name:{{ Test.name }}</div>
<div style="display: flex; flex-direction: column">
<button @click="change">change</button>
<button @click="bulkChange">批量change</button>
<button @click="funChange">工厂函数实现批量change</button>
<button @click="allChange">必须修改全部的写法(不推荐写法)</button>
<button @click="actionsChange">使用actions修改</button>
</div>
</template>
<script setup lang="ts">
import { useTestStore } from "./store";
const Test = useTestStore();
const change = () => {
Test.current++;
};
const bulkChange = () => {
Test.$patch({
current: 888,
name: "小4",
});
};
const funChange = () => {
Test.$patch((state) => {
(state.current = 999), (state.name = "小3");
});
};
const allChange = () => {
Test.$state = {
...Test.$state,
name: "小2",
};
};
const actionsChange = () => {
Test.setCurrent(555);
};
</script>
<style lang="scss" scoped></style>
结果展示:
五、解构store
在Pinia是不允许直接解构state的数据的,数据会失去响应式。
const Test = useTestStore()
// 直接解构失去响应式
const { current, name } = Test
差异对比:
<template>
<div>响应式的值:{{ Test.current }}</div>
<div>解构出的非响应式值:{{ current }}--{{ name }}</div>
<button @click="change">change</button>
</template>
<script setup lang="ts">
import { useTestStore } from "./store";
const Test = useTestStore();
const { current, name } = Test;
const change = () => {
Test.current++;
};
</script>
<style lang="scss" scoped></style>
结果展示:
解决方案:
使用pinia的storeToRefs()
方式将数据响应式化。如下:
// 引入
import { storeToRefs } from "pinia";
import { useTestStore } from "./store";
const Test = useTestStore();
// 响应式化
const { current, name } = storeToRefs(Test);
const change = () => {
Test.current++;
};
结果展示:
六、actions和getters
store/index.ts
import { defineStore } from "pinia";
import { Names } from "./store-name";
type User = {
name: string,
age: number
}
let result:User = {
name: '小5',
age: 18
}
const Login = (): Promise<User> => {
return new Promise((resolve) => {
setTimeout(() => {
resolve({
name: "小4",
age: 17
})
})
})
}
export const useTestStore = defineStore(Names.TEST, {
state: () => {
return {
// 类型断言
// <type>{} 通常用于类型断言,表示将某个值强制转换成指定的类型。
user: <User>{},
name: 'xiao5'
}
},
// 类似于computed可以帮我们修饰我们的值
getters: {
newName(): string {
return `$-${this.name}`
}
},
// 可以操作异步 和 同步
actions: {
// 不能写箭头函数 否则this会指向错误
// 同步
setUser1() {
this.user = result
this.name = '小8'
},
// 异步
async setUser2() {
const result = await Login()
this.user = result
this.name = '小7'
},
}
})
App.vue
<template>
<div>actions:{{ Test.user }}</div>
<button @click="change1">同步</button>
<button @click="change2">异步</button>
<div>getters:{{ Test.newName }}</div>
</template>
<script setup lang="ts">
import { useTestStore } from "./store";
const Test = useTestStore();
const change1 = () => {
Test.setUser1();
};
const change2 = () => {
Test.setUser2();
};
</script>
<style lang="scss" scoped></style>
结果展示:
另外,多个actions可以相互调用,多个getters也可以相互调用。
七、API
1.$reset
重置store到它的初识状态
import { useTestStore } from "./store";
const Test = useTestStore();
const reset = () => {
Test.$reset();
};
2.$subscribe
用于订阅state
的改变,只要有state的变化就会触发这个函数
$subscribe的第一个参数是个回调函数
Test.$subscribe((args, state) => {
console.log("======>", args);
console.log("======>", state);
});
返回值args
主要包括effect
、target
、storeId
等信息,state
是state数据变化后的状态。如下图
第二个参数是是一个配置对象
Test.$subscribe(
(args, state) => {
console.log("======>", args);
console.log("======>", state);
},
{
detached: true,
deep: true,
flush: "post",
}
);
这三种配置详细如下:
detached(脱离状态):指组件从其父级组件或 DOM 树中被移除的状态。在这种状态下,组件不再接收更新,并且可以被销毁。Vue 2.x 中通过调用 $destroy() 方法来销毁组件,Vue 3.x 中则使用 teleport、keepAlive 等组合来控制组件的生命周期。
deep(深度监听):指对一个对象进行深度监听,在该对象的所有属性的值发生改变时,都能够得到通知。在 Vue.js 中,可以使用 vm.$watch() 方法来实现对数据的深度监听,Vue 3.x 中也提供了相应的 API 实现深度监听。
flush(刷新策略):指一种更新数据后如何刷新页面的策略。在 Vue.js 中,默认的刷新策略是异步批处理模式(nextTick 模式),即将所有数据的更新操作放入一个队列中,在下一个 tick 执行更新操作,以减少不必要的 DOM 操作和提高性能。除此之外,Vue.js 还支持同步刷新(sync)、立即刷新(pre)等刷新策略。
3.$onAction
用于订阅actions的调用,只要有actions被调用就会触发这个函数。
Test.$onAction((args) => {
console.log(args);
});
返回值args
主要包括after
回调,args
(actions传递的参数),name
(触发的actions名字),onError
(错误回调),store
(store实例)等。如下图
八、pinia插件
pinia 和 vuex 都有一个通病 页面刷新状态会丢失
我们可以写一个pinia 插件缓存他的值
参考博客:满哥牛