Pinia是官网推荐Vue3项目中作为全局状态管理的新工具。
官方推出的全局状态管理工具目前有Vuex和Pinia,Pinia的设计更贴近Vue3组合式API的用法。
从 2022-02-07 在 Vue 3 被设置为默认版本开始, Pinia 已正式被官方推荐作为全局状态管理的工具。
官网:pinia.vuejs.org/ 笔记整理-主要参考:vue3.chengpeiquan.com/
安装和启动
-
npm install pinia- package.json中可查看到Pinia和版本号
- 在main.js中添加/多出两行代码
-
import {createPinia} from 'pinia'//导入Pinia -
.use(createPinia())//启用Pinia
状态树的结构
| 作用 | Vue Component | Vuex | Pinia |
|---|---|---|---|
| 数据管理 | data | state | state |
| 数据计算 | computed | getters | getters |
| 行为方法 | methods | mutations/actions | actions |
补充说明:在行为方法部分去掉了 mutations (同步操作)和 actions (异步操作)的区分,更接近组件的结构,入门成本会更低一些。
创建Store(Pinia核心)
- 特点:Store通过defineStore创建
-
管理方案:在src下创建stores,并添加index.ts
- 直接在index.ts中创建Store
- 通过index.ts引入其他.ts文件,在其他.ts文件中创建Store
-
入参形式,必须为Store指定一个唯一ID
- 函数命名规范:以use开头(e.g. useUserStore\useGameStore)
- 默认导出:是使用export const不是export default
-
//形式1:接收两个参数 import {defineStore} from 'pinia' export const useStore = defineStore('main',{ //Store选项 }) //形式2:接收一个参数 import {defineStore} from 'pinia' export const useStore = defineStore({ id:'main', //Store选项 })
管理State
给Store添加state
- 特点:通过箭头函数形式
state:()=>({})或state:()=>{return{}}返回数据
// src/stores/index.ts
import { defineStore } from 'pinia'
export const useStore = defineStore('main', {
// 写法1:不显式 return
state: () => ({
message: 'Hello World',
}),
// 写法2:显式 return
state: () => {
return {
message: 'Hello World',
}
},
// ...
})
手动指定类型
- 特点:可通过
as或<>指定类型
- 例子:
[] as string[]、<string[]>[]
// ...
export const useStore = defineStore('main', {
state: () => {
return {
message: 'Hello World',
// 通过 as 关键字指定 TS 类型
randomMessages: [] as string[],
// ...
// 通过 as 关键字指定 TS 类型
randomMessages: [] as string[],
//Pinia 会帮推导成 never[]
randomMessages: [],
}
},
// ...
})
获取和更新state
- Pinia数据是挂在store上(store.message),Vuex是在store.state上(store.state.message)
import { defineComponent,toRefs,toRef } from 'vue'
import { useStore } from '@/stores'
import { storeToRefs } from 'pinia'
export default defineComponent({
setup() {
// 像 useRouter 那样定义一个变量拿到实例
const store = useStore()
// 获取方式1:直接通过实例来获取数据(不推荐)
console.log(store.message)
//获取方式2:通过计算属性computed拿值传给template(message1)使用(only read)
const message1=computed(()=>store.message)
//更新数据方式1:定义computed变量是配置getter和setter
const message = computed({
get:()=>store.message,
set(newVal){
store.message=newVal
}
})
message.value='new value'
console.log(store.message)//new value
//更新数据方式2,获取方式3:使用storeToRefs API(转换为ref变量)
//通过storeToRefs拿到响应性的message
const {message}=storeToRefs(store)
console.log(message.value)
message.value='new message-value'
//更新数据方式3,获取方式4:使用Vue的toRefs或toRef API——Vue API
const message = toRefs(store)//转换所有字段为ref变量
const message = toRef(store,'message')//转换一个字段为ref变量
//action方法(Pinia所有操作都在action,不区分同步和异步,Vuex更新数据需通过mutation提交 并且异步操作需通过action触发mutation)
// 这种方式需要把整个 store 给到 template 去渲染数据
return {
store,
message1,
message
}
},
})
批量更新state(修改多个state数据)——$patch(Pinia API)
- 特点:只能修改已定义的数据,传进去的函数只能是同步函数,不可是异步函数(异步 JavaScript 简介 - 学习 Web 开发 | MDN)
- 写法:store.$patch(对象/函数)
// 继续用前面的数据,这里会打印出修改前的值
console.log(JSON.stringify(store.$state))
// 输出 {"message":"Hello World","randomMessages":[]}
/**
* 注意这里,传入了一个对象
*/
store.$patch({
message: 'New Message',
randomMessages: ['msg1', 'msg2', 'msg3'],
})
/**
* 注意这里,这次是传入了一个函数
*/
store.$patch((state) => {
state.message = 'New Message'
// 数组改成用追加的方式,而不是重新赋值
for (let i = 0; i < 3; i++) {
state.randomMessages.push(`msg${i + 1}`)
}
})
// 这里会打印出修改后的值
console.log(JSON.stringify(store.$state))
// 输出 {"message":"New Message","randomMessages":["msg1","msg2","msg3"]}
全量更新state——store.$state
- 特点:store.$state属性本身可以直接赋值,赋值时必须遵循state原有的数据和对应的类型。
- 例子:更新数据(state还是会保持响应性)
store.$state = {
message: 'New Message',
randomMessages: ['msg1', 'msg2', 'msg3'],
}
重置state——$reset
- 特点:用于重置整个state树为初始数据
- 补充:可以和setTimeout结合使用
// 修改数据
store.message = 'New Message'
console.log(store.message) // 输出 New Message
// 3s 后重置状态
setTimeout(() => {
store.$reset()
console.log(store.message) // 输出最开始的 Hello World
}, 3000)
订阅state——$subscribe
-
特点:类似于watch,它只会在state被更新时触发一次,组件被卸载时删除
- $subscribe接受两个参数
- 第一个参数是callback函数,必传;第二个入参时一些选项,可选
// $subscribe 部分的 TS 类型
// ...
$subscribe(
callback: SubscriptionCallback<S>,
options?: { detached?: boolean } & WatchOptions
): () => void
// ...
-
添加订阅
-
接收的第一个参数是必传的callback函数
-
callback有两个入参:mutation(本次事件的一些信息)、state(当前实例的state)
-
mutation包含以下数据
- storeId:发布本次订阅通知的Pinia实例的唯一ID(创建Store时指定)
- type:有3个值,返回direct代表直接更改数据;返回
patch object代表时通过传入一个对象更改;返回patch function则代表是通过传入一个函数更改 - events:触发本次订阅通知的事件列表
- payload:通过传入一个函数更改时,传递进来的荷载信息,只有type为
patch object时有
-
补充:若希望组件被卸载时,不删除订阅,可传递第2个参数options(也可以搭配 watch API 的选项一起用)
-
// 可以在 state 出现变化时,更新本地持久化存储的数据
store.$subscribe(
(mutation, state) => {
localStorage.setItem('store', JSON.stringify(state))
},
{
detached: true
}
)
-
删除订阅
- 使用场景:detached为true,需要手动卸载订阅
unsubscribe() - 特点:与watch API 的机制非常相似, 它也是返回 一个取消监听的函数 用于移除指定的 watch
- 使用场景:detached为true,需要手动卸载订阅
// 定义一个退订变量,它是一个函数
const unsubscribe = store.$subscribe(
(mutation, state) => {
// ...
},
{ detached: true }
)
// 在合适的时期调用它,可以取消这个订阅
unsubscribe()
管理getters
- 特点:Pinia的getters用于计算数据
给Store添加getter
-
添加普通的getter
- 特点:通过入参的state拿当前数据(Pinia官方推荐箭头函数)
-
// src/stores/index.ts import { defineStore } from 'pinia' export const useStore = defineStore('main', { state: () => ({ message: 'Hello World', }), // 定义一个 fullMessage 的计算数据 getters: { fullMessage : ( state ) => `The message is " ${state.message} ".` , }, // ... })
-
添加引用getter的getter
-
特点:
- 引用另一个getter的值来返回数据,不能用箭头函数,需定义成普通函数
- 普通函数内容通过
this调用当前Store上的数据和方法 - 普通函数的TS返回类型要显式标注
-
export const useStore = defineStore('main', {
state: () => ({
message: 'Hello World',
}),
getters: {
fullMessage: (state) => `The message is "${state.message}".`,
// 这个 getter 返回了另外一个 getter 的结果
emojiMessage(): string {
return `🎉🎉🎉 ${this.fullMessage}`
},
},
})
-
给getter传递参数
-
特点:本身不支持参数(与Vuex一样),支持返回一个具备入参的函数
-
import { defineStore } from 'pinia' export const useStore = defineStore('main', { state: () => ({ message: 'Hello World', }), getters: { // 定义一个接收入参的函数作为返回值 signedMessage: (state) => { return (name: string) => `${name} say: "The message is ${state.message}".` }, }, //调用时 const signedMessage = store.signedMessage('Petter') console.log('signedMessage', signedMessage) // Petter say: "The message is Hello World". }) -
注意点:
- 这个getter只是调用的函数作用,不再有缓存
- 通过变量定义这个getter数据,该变量只是普通变量,不具备响应性
-
// 通过变量定义一个值 const signedMessage = store.signedMessage('Petter') console.log('signedMessage', signedMessage) // Petter say: "The message is Hello World". // 2s 后改变 message setTimeout(() => { store.message = 'New Message' // signedMessage 不会变 console.log('signedMessage', signedMessage) // Petter say: "The message is Hello World". // 必须这样再次执行才能拿到更新后的值 console.log('signedMessage', store.signedMessage('Petter')) // Petter say: "The message is New Message". }, 2000)
-
获取和更新getter
getter和state都属于数据管理,读取和赋值是一样的
管理actions
- 特点:Pinia只需要用actions就可解决数据操作;Vuex需要区分mutation/actions
给Store添加action(src/store/index.ts)
-
特点:
- 在actions中若访问当前实例的state或getter,通过this操作
- this是指当前的Store实例
- actions方法中有其他函数调用实例,需写成箭头函数(来提升this)
// src/stores/index.ts
import { defineStore } from 'pinia'
export const useStore = defineStore('main', {
state: () => ({
message: 'Hello World',
}),
actions: {
// 异步更新 message
async updateMessage(newMessage: string): Promise<string> {
return new Promise((resolve) => {
setTimeout(() => {
// 这里的 this 是当前的 Store 实例
this.message = newMessage
resolve('Async done.')
}, 3000)
})
},
// 同步更新 message
updateMessageSync(newMessage: string): string {
// 这里的 this 是当前的 Store 实例
this.message = newMessage
return 'Sync done.'
},
},
})
调用action
- 特点:像调用普通函数一样(不需和Vuex一样执行commit或dispatch)
export default defineComponent({
setup() {
const store = useStore()
const { message } = storeToRefs(store)
// 立即执行
console.log(store.updateMessageSync('New message by sync.'))
// 3s 后执行
store.updateMessage('New message by async.').then((res) => console.log(res))
return {
message,
}
},
})
添加多个Store(用于维护不同需求模块的数据状态)
目录结构及使用
目录机构推荐
src
└─stores
│ # 入口文件
├─index.ts
│ # 多个 store
├─user.ts
├─game.ts
└─news.ts
以index.ts作为统一的入口文件,index.ts中内容可如下
export * from './user'
export * from './game'
export * from './news'
index.ts中其他文件写法,如./user
// src/stores/user.ts
export const useUserStore = defineStore('user', {
// ...
})
使用时
import { useUserStore } from '@/stores'
在Vue组件/TS文件中使用
- 注意点:切记每个 Store 的 ID 必须不同,如果 ID 重复,会以先定义的为有效值,后续定义的会和前面一样
- 场景:目前有一个
userStore是管理当前登录用户信息,gameStore是管理游戏的信息,而 “个人中心” 这个页面需要展示 “用户信息” ,以及 “该用户绑定的游戏信息”
import { defineComponent, onMounted, ref } from 'vue'
import { storeToRefs } from 'pinia'
// 这里导入要用到的 Store
import { useUserStore, useGameStore } from '@/stores'
import type { GameItem } from '@/types'
export default defineComponent({
setup() {
// 先从 userStore 获取用户信息(已经登录过,所以可以直接拿到)
const userStore = useUserStore()
const { userId, userName } = storeToRefs(userStore)
// 使用 gameStore 里的方法,传入用户 ID 去查询用户的游戏列表
const gameStore = useGameStore()
const gameList = ref<GameItem[]>([])
onMounted(async () => {
gameList.value = await gameStore.queryGameList(userId.value)
})
return {
userId,
userName,
gameList,
}
},
})
Store之间互相引用
// src/stores/message.ts
import { defineStore } from 'pinia'
// 导入用户信息的 Store 并启用它
import { useUserStore } from './user'
const userStore = useUserStore()
export const useMessageStore = defineStore('message', {
state: () => ({
message: 'Hello World',
}),
getters: {
// 这里就可以直接引用 userStore 上面的数据了
greeting: () => `Welcome, ${userStore.userName}!`,
},
})
const messageStore = useMessageStore()
console.log(messageStore.greeting) // Welcome, Petter!
Pinia专属插件的使用
查找插件
插件的命名格式:pinia-plugin-*
pinia-plugin-persistedstate
- 是一个让数据持久化存储的 Pinia 插件
-
插件用法
- 安装:npm i pinia-plugin-persistedstate
- 激活(在main.ts)
-
// src/main.ts import { createApp } from 'vue' import App from '@/App.vue' import { createPinia } from 'pinia' // 导入 Pinia import piniaPluginPersistedstate from 'pinia-plugin-persistedstate' // 导入 Pinia 插件 const pinia = createPinia() // 初始化 Pinia pinia.use(piniaPluginPersistedstate) // 激活 Pinia 插件 createApp(App) .use(pinia) // 启用 Pinia ,这一次是包含了插件的 Pinia 实例 .mount('#app') - 在一个Store启用,只需添加
persist:true -
// src/stores/message.ts import { defineStore } from 'pinia' import { useUserStore } from './user' const userStore = useUserStore() export const useMessageStore = defineStore('message', { state: () => ({ message: 'Hello World', }), getters: { greeting: () => `Welcome, ${userStore.userName}`, }, // 这是按照插件的文档,在实例上启用了该插件,这个选项是插件特有的 persist: true, })