Pinia(Vue 的专属状态管理库)
Pinia 和 Vuex 的区别
设计理念和架构
Vuex 采用集中式架构,所有状态存储在一个全局状态树中,通过mutations
和actions
来修改和处理状态。
Pinia 采用去中心化的架构,每个模块有自己的状态,这使得 Pinia 在代码分割和模块化方面更加灵活。
TypeScript支持
Vuex 早期版本对 TypeScript 支持较弱,需要额外插件实现类型检查。
Pinia 从设计之初就考虑到了 TypeScript,提供了更好的类型推导和检查支持。
体积和性能
Pinia的体积较小,加载速度快,且因其轻量级设计,性能优于Vuex。这得益于Pinia充分利用了Vue 3的响应式系统优化。
使用难度
Pinia 的API设计更简洁,没有 Vuex 的mutations
和modules
概念,这使得 Pinia 更容易理解和使用。Pinia 更适合初学者和快速开发项目,而 Vuex 则更适合复杂的项目和对状态管理有更高要求的开发者。
社区支持和生态系统
Vuex 作为 Vue.js 官方出品的状态管理库,拥有更强的社区支持和丰富的文档。而 Pinia 虽然是一个较新的库,但在 Vue 3 项目中因其轻量级和简洁性而受到青睐。
总结来说,选择Pinia还是Vuex取决于项目的具体需求和开发者的个人偏好。对于需要快速开发和维护简单性的项目,Pinia可能是更好的选择;而对于需要高度模块化和集成的大型复杂项目,Vuex可能更适合。
Vuex 的理解和学习 https://blog.csdn.net/weixin_54092687/article/details/140280584
Pinia 官方文档 https://pinia.vuejs.org/zh/
进一步学习Pinia的其他核心概念和服务器渲染 https://blog.csdn.net/weixin_54092687/article/details/140561225
定义 Store
// 第一步 -- src/store/store.ts 定义仓库
import { defineStore } from 'pinia'
export const useMyPiniaStore = defineStore('myPinia', { ... });
// 第二步 -- main.ts 注册 Pinia
import { createApp } from 'vue'
import App from './App.vue'
import { createPinia } from 'pinia'
const pinia = createPinia();
createApp(App).use(pinia).mount('#app')
// 第三步 -- 组件中使用Store
<script setup>
import { useMyPiniaStore } from '@/store/store'
const store = useMyPiniaStore()
</script>
Store 参数
第一个参数 - - - id
仓库名称,必填,唯一性,习惯格式use__Store
。
第二个参数 - - - 接受两种类型
Option 对象:带有state
|getters
|actions
属性。
Setup 函数:ref() => state
|computed() => getters
|function() => actions
Setup Store
- 要让 pinia 正确识别 state,你必须在 setup store 中返回 state 的所有属性。这意味着不能在 store 中使用私有属性。
- Setup store 比 Option Store 带来了更多的灵活性,因为可以在一个 store 内创建侦听器,并自由地使用任何组合式函数。
- Setup store 可以依赖于全局提供的属性,比如路由。任何应用层面提供的属性都可以在 store 中使用 inject() 访问,就像在组件中一样。
// Option 对象
defineStore('myPinia', {
state: () => { return { } },
getters: { },
actions: { }
});
// Setup 函数
defineStore('myPinia', () => {
const count = ref(0)
const doubleCount = computed(() => { })
function increment() { }
return { count, doubleCount, increment }
})
解构 Store
storeToRefs 解构 store 中的 状态 或 getter,跳过所有的 action 或非响应式 (不是 ref 或 reactive) 的属性。
action 或 非响应式 (不是 ref 或 reactive) 的属性 直接在 Store 中解构。
// 定义
import { defineStore } from 'pinia'
import { computed, ref } from 'vue';
export const useMyPiniaStore = defineStore('myPinia', () => {
const count = ref(1);
const doubleCount = computed(() => count.value * 2)
const increment = () => { count.value++ }
const unRective = '非响应式数据'
return { count, doubleCount, increment, unRective }
});
// 解构
<script setup>
import { useMyPiniaStore } from '../store/store'
import { storeToRefs } from 'pinia'
const store = useMyPiniaStore()
const { count, doubleCount } = storeToRefs(store)
const { increment, unRective } = store
</script>
State
// 方式1 -- 不设置状态类型
export const useMyPiniaStore = defineStore('myPinia', {
state: () => { return { count: 0 } }
});
// 方式2 -- ts 严格模式 定义接口规定数据的类型
interface UserInfo {
name: string
age: number,
count: number
}
defineStore('myPinia', {
state: () => {
return {
userList: [] as UserInfo[],
user: null as UserInfo | null,
count: 0 as UserInfo['count']
}
}
})
访问 state
// 注意,新的属性如果没有在 state() 中被定义,则不能被添加。它必须包含初始状态。
// 例如:如果 secondCount 没有在 state() 中定义,我们无法执行 store.secondCount = 2。
const store = useMyPiniaStore()
store.count++
重置 state
// 选项式API(options API)
const store = useMyPiniaStore()
store.$reset()
// 组合式API(Composition API)
defineStore('myPinia', () => {
const count = ref(0)
const $reset = () => { count.value = 0 }
return { count, $reset }
})
选项式 API 中的 mapState 和 mapWritableState
// mapState
import { mapState } from 'pinia'
import { useMyPiniaStore } from '../store/store'
export default {
computed: {
...mapState(useMyPiniaStore, ['count']) // this.count => store.count
...mapState(useMyPiniaStore, {
myOwnName: 'count', // this.myOwnName => store.count
double: store => store.count * 2 // 定义一个函数获取 store.count
})
}
}
// mapWritableState -- 不能像 mapState() 那样传递一个函数
export default {
computed: {
...mapWritableState(useMyPiniaStore, ['count']) // this.count => store.count
...mapWritableState(useMyPiniaStore, { myOwnName: 'count' }) // this.myOwnName => store.count
}
}
对于像数组这样的集合,并不一定需要使用 mapWritableState(),mapState() 也允许你调用集合上的方法,除非想用 cartItems = [] 替换整个数组。
变更 state
// 1. patch 方法。允许用一个 state 的补丁对象在同一时间更改多个属性
store.$patch({
count: store.count + 1,
age: 120,
name: 'DIO'
})
// 2. 有些变更很难实现或者很耗时:任何集合的修改(例如,向数组中添加、移除一个元素或是做 splice 操作)
//都需要创建一个新的集合。因此,$patch 方法也接受一个函数来组合这种难以用补丁对象实现的变更。
store.$patch((state) => {
state.items.push({ name: 'shoes', quantity: 1 })
state.hasChanged = true
})
替换 state
// 不能完全替换掉 store 的 state,因为那样会破坏其响应性。但是可以 patch 它。
store.$patch({ count: 66 })
// 也可以通过变更 pinia 实例的 state 来设置整个应用的初始 state。
pinia.state.value = {}
订阅 state
类似于 Vuex 的 subscribe 方法,你可以通过 store 的
$subscribe()
方法侦听 state 及其变化。比起普通的 watch(),使用 $subscribe() 的好处是 subscriptions 在 patch 后只触发一次。
cartStore.$subscribe((mutation, state) => {
// import { MutationType } from 'pinia'
mutation.type // 'direct' | 'patch object' | 'patch function'
// 和 cartStore.$id 一样
mutation.storeId // 'cart'
// 只有 mutation.type === 'patch object'的情况下才可用
mutation.payload // 传递给 cartStore.$patch() 的补丁对象。
// 每当状态发生变化时,将整个 state 持久化到本地存储。
localStorage.setItem('cart', JSON.stringify(state))
})
默认情况下,state subscription 会被绑定到添加它们的组件上 (如果 store 在组件的 setup() 里面)。这意味着,当该组件被卸载时,它们将被自动删除。如果想在组件卸载后依旧保留它们,请将 { detached: true } 作为第二个参数,以将 state subscription 从当前组件中分离。
<script setup>
const someStore = useSomeStore()
// 此订阅器即便在组件卸载之后仍会被保留
someStore.$subscribe(callback, { detached: true })
</script>
可以在 pinia 实例上使用 watch() 函数侦听整个 state。
watch(
pinia.state,
(state) => {
// 每当状态发生变化时,将整个 state 持久化到本地存储。
localStorage.setItem('piniaState', JSON.stringify(state))
},
{ deep: true }
)
Getter
Getter 完全等同于 store 的 state 的计算值。
// option对象 -- 定义getter
export const useMyPiniaStore = defineStore('myPinia', {
state: () => ({
count: 0
}),
getters: {
//方式1 -- 基本使用
doubleCount: (state) => state.count * 2,
//方式2 -- 使用其他 getter,在 TypeScript 中必须定义返回类型。
doublePlusOne(): number {
return this.doubleCount + 1
},
//方式3 -- 返回一个函数
countAddNum: (state) => {
return (num: number) => state.count + num
},
//方法4 -- 访问其他 store 的getter
otherGetter: (state) => {
const otherStore = useOtherStore()
return state.count + otherStore.count
}
}
})
在 setup() 中使用 getter
<script setup>
import { useMyPiniaStore } from '../store/store'
const store = useMyPiniaStore()
store.count = 3
store.doubleCount // 6
</script>
使用 mapState 辅助函数映射 getter
import { mapState } from 'pinia'
import { useMyPiniaStore } from '../store/store'
export default {
computed: {
...mapState(useMyPiniaStore, ['doubleCount']), // this.doubleCount => store.doubleCount
...mapState(useMyPiniaStore, {
myOwnName: 'doubleCount', // this.myOwnName => store.doubleCount
double: (store) => store.doubleCount // double => store.doubleCount
})
}
}
Action
定义和使用 action
action 相当于组件中的 method。
action 可以是异步的。
// 定义 actions
export const useMyPiniaStore = defineStore('myPinia', {
state: () => { return { count: 6 } },
actions: {
increment() { this.count++ },
randomizeCounter() { this.count = Math.round(100 * Math.random()) },
// 异步
asyncCount() { setTimeout(() => this.count++, 1000) },
// 接收参数
countAddNumber(num: number) { this.count += num }
}
});
// 使用 action
<template>
<button @click="store.increment()">increment</button>
<button @click="store.randomizeCounter()">randomizeCounter</button>
<button @click="store.asyncCount()">asyncCount</button>
<button @click="store.countAddNumber(100)">countAddNumber</button>
<p>{{ store.count }}</p>
</template>
<script setup>
import { useMyPiniaStore } from './store/store'
const store = useMyPiniaStore()
</script>
mapActions
import { mapActions } from 'pinia'
import { useMyPiniaStore } from '../store/store'
export default {
methods: {
...mapActions(useMyPiniaStore, ['increment']) // this.increment() => store.increment()
...mapActions(useMyPiniaStore, { myOwnName: 'increment' }) // this.myOwnName() => store.increment()
},
}
订阅 action
通过
store.$onAction()
来监听 action 和它们的结果。传递给它的回调函数会在 action 本身之前执行。after
表示在promise
解决之后,允许你在 action 解决后执行一个回调函数。onError
允许你在 action抛出错误 / reject
时执行一个回调函数。
const unsubscribe = useMyPiniaStore.$onAction(
({
name, // action 名称
store, // store 实例,类似 `useMyPiniaStore`
args, // 传递给 action 的参数数组
after, // 在 action 返回或解决后的钩子
onError // action 抛出或拒绝的钩子
}) => {
const startTime = Date.now() // 为这个特定的 action 调用提供一个共享变量
console.log(`Start "${name}" with params [${args.join(', ')}].`) // 这将在执行 "store "的 action 之前触发。
after((result) => { // 这将在 action 成功并完全运行后触发。它等待着任何返回的 promise
console.log(`Finished "${name}" after ${Date.now() - startTime}ms.\nResult: ${result}.`)
})
onError((error) => { // 如果 action 抛出或返回一个拒绝的 promise,这将触发
console.warn(`Failed "${name}" after ${Date.now() - startTime}ms.\nError: ${error}.`)
})
}
)
// 手动删除监听器
unsubscribe()
默认情况下,action 订阅器会被绑定到添加它们的组件上(如果 store 在组件的 setup() 内)。当该组件被卸载时,它们将被自动删除。如果想在组件卸载后依旧保留它们,请将 true 作为第二个参数传递给 action 订阅器,将其从当前组件中分离。
<script setup>
const someStore = useSomeStore()
someStore.$onAction(callback, true) // 此订阅器即便在组件卸载之后仍会被保留
</script>
总结
Pinia的好处主要体现在简单易用、TypeScript支持、分治管理、性能优化等方面。
简单易用:Pinia的API设计简洁明了,易于理解和使用。与Vuex相比,它不需要定义复杂的store和module,而是采用类似于Vue组件的方式来处理状态管理,使得状态管理更加直观和易于上手。
TypeScript支持:Pinia内置了对TypeScript的支持,能够提供类型安全的状态管理,有效避免了类型错误的问题。这对于使用TypeScript开发的Vue项目来说,是一个重要的优势。
分治管理:Pinia支持将状态分割成多个模块,每个模块可以独立管理自己的状态。这种分治的设计使得应用状态更加清晰可控,有助于提高代码的可维护性和可读性。
性能优化:Pinia内部使用了响应式编程的技术,提高了状态管理的性能和响应速度。此外,它还支持使用插件来进行状态的持久化和调试,进一步提高了开发效率。
综上所述,Pinia作为一个基于Vue 3的状态管理库,通过其简洁的API设计、内置的TypeScript支持、模块化的状态管理以及性能优化等特点,为Vue项目的开发提供了高效且便捷的状态管理解决方案。