前言
Pinia是新一代的状态管理器,由Vue团队中核心成员所开发的,同时也被认为是下一代的Vuex,也就是Vuex5.x。
其特点如下:
- 完全支持
typescript - 足够轻量,压缩后体积只有1.6kb
- 模块化的设计,在打包时引入的每一个
store都可以自动拆分 - 没有模块嵌套,只有
store概念,store之间可以自由使用,实现更好的代码分隔 actions支持同步和异步- 支持
Vue Devtools
安装
yarn add pinia
// or
npm install pinia
// or
pnpm add pinia
使用
在vue3.x版本中直接通过createPinia创建Pinia实例即可,然后在main.ts中使用app.use(pinia)去加载pinia
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import App from './App.vue'
const pinia = createPinia()
const app = createApp(App).use(pinia)
app.mount('#app')
但如果想在vue2.x版本中使用的话,需要注册PiniaVuePlugin插件并且需要安装@vue/composition-api依赖包
https://github.com/vuejs/pinia
npm install pinia @vue/composition-api
// https://pinia.vuejs.org/getting-started.html#installation
import { createPinia, PiniaVuePlugin } from 'pinia'
Vue.use(PiniaVuePlugin)
const pinia = createPinia()
new Vue({
el: '#app',
// other options...
pinia,
})
因为在pinia中只有store的概念,所以下面说一下它的基本用法
创建store
pinia提供了defineStore方法,并且支持两种方式可以直接创建store
import { defineStore } from 'pinia
const pinia = defineStore({id: '唯一标识', ...})
const pinia = defineStore('唯一标识', {...})
定义state
import { defineStore } from 'pinia'
export const userStore = defineStore({
id: 'user',
state: () => {
return {
name: '张三'
}
},
getters: {...},
actions: {...},
})
获取state
<template>
<div>
普通获取:{{user.name}}
computed: {{_name}}
解构:{{name}}
</div>
</template>
<script lang="ts" setup>
import { userStore } from '../store/user'
import { computed } from 'vue'
import { storeToRefs } from 'pinia'
// 普通获取
const user = userStore()
// computed获取
const _name = computed(() => user.name)
// 解构,但是会失去响应,可以用pinia的 storeToRefs避免失去响应
const { name } = storeToRefs(user)
</script>
修改state
直接修改(不推荐)
...其他代码
<script lang="ts" setup>
import { userStore } from '../store/user'
const user = userStore()
// 修改state(不建议)
user.name = '李四'
</script>
建议通过actions去修改
export const useUserStore = defineStore({
id: 'user',
state: () => { return { name: '张三' } },
actions: { updateName (name) { this.name = name } }
})
<script lang="ts" setup>
import { useUserStore } from '@/store/user'
const userStore = useUserStore()
userStore.updateName('王五')
</script>
Getters
export const useUserStore = defineStore({
id: 'user',
state: () => {
return {
name: '张三'
}
},
getters: {
fullName: (state) => {
return state.name + '真帅'
}
}
})
useUserStore.fullName // 张三真帅
Actions
export const useUserStore = defineStore({
id: 'user',
actions: {
async getUserName() {
const { data } = await api.getUserName()
return data
}
}
})
源码分析
以上简单说了一些Pinia的使用方式,下面来简单分析一下源码。
本文主要从创建Pinia(createPinia)和定义store(defineStore)两个主要函数进行分析。
createPinia(方法路径:packages/pinia/src/index.ts)
export function createPinia(): Pinia {
...
// 当前pinia实例
const pinia: Pinia = markRaw({
// vue的插件机制,对外暴露install方法
install(app: App) {
// this allows calling useStore() outside of a component setup after
// installing pinia's plugin
// 设置当前活跃的 pinia,当存在多个活跃的pinia时,方便获取
setActivePinia(pinia)
if (!isVue2) {
pinia._a = app
// 通过provide传递pinia实例,提供给后续使用
app.provide(piniaSymbol, pinia)
// 设置全局属性 $pinia
app.config.globalProperties.$pinia = pinia
/* istanbul ignore else */
if (__DEV__ && IS_CLIENT) {
// @ts-expect-error: weird type in devtools api
registerPiniaDevtools(app, pinia)
}
toBeInstalled.forEach((plugin) => _p.push(plugin)) // 加载pinia插件
toBeInstalled = []
}
},
// pinia对外暴露的插件用法
use(plugin) {
if (!this._a && !isVue2) {
toBeInstalled.push(plugin)
} else {
_p.push(plugin)
}
return this
},
_p,
// it's actually undefined here
// @ts-expect-error
_a: null,
_e: scope,
_s: new Map<string, StoreGeneric>(),
state, // 所有状态
})
// pinia devtools rely on dev only features so they cannot be forced unless
// the dev build of Vue is used
if (__DEV__ && IS_CLIENT) {
// 集成 vue devtools
pinia.use(devtoolsPlugin)
}
return pinia
}
以上注释已表明
defineStore(方法路径:packages/pinia/src/store.ts 822行)
export function defineStore(
// TODO: add proper types from above
idOrOptions: any,
setup?: any,
setupOptions?: any
): StoreDefinition {
let id: string
let options
const isSetupStore = typeof setup === 'function'
// 根据传参格式,获取id和otions,可以defineStore(id, options),也可以defineStore({id: '唯一值', ...})
if (typeof idOrOptions === 'string') {
id = idOrOptions
// the option store setup will contain the actual options in this case
options = isSetupStore ? setupOptions : setup
} else {
options = idOrOptions
id = idOrOptions.id
}
// 返回Store
function useStore(pinia?: Pinia | null, hot?: StoreGeneric): StoreGeneric {
// 通过vue的getCurrentInstance方法获取vue实例
const currentInstance = getCurrentInstance()
// 判断pinia是否存在,不存在的话通过inject(piniaSymbol)获取(install时提供的app.provide(piniaSymbol, pinia)
pinia =
// in test mode, ignore the argument provided as we can always retrieve a
// pinia instance with getActivePinia()
(__TEST__ && activePinia && activePinia._testing ? null : pinia) ||
(currentInstance && inject(piniaSymbol))
// 设置当前活跃的pinia实例,有多个pinia实例时,方便获取当前活跃实例
if (pinia) setActivePinia(pinia) // 路径:packages/pinia/src/rootStore.ts
// activePinia不存在时,报错提示入口use(pinia)
if (__DEV__ && !activePinia) {
throw new Error(
`[🍍]: getActivePinia was called with no active Pinia. Did you forget to install pinia?\n` +
`\tconst pinia = createPinia()\n` +
`\tapp.use(pinia)\n` +
`This will fail in production.`
)
}
pinia = activePinia!
// 初始化时pinia._s.has(id)没有值
if (!pinia._s.has(id)) {
// creating the store registers it in `pinia._s`
// const isSetupStore = typeof setup === 'function'
if (isSetupStore) {
createSetupStore(id, setup, options, pinia)
} else {
createOptionsStore(id, options as any, pinia)
}
/* istanbul ignore else */
if (__DEV__) {
// @ts-expect-error: not the right inferred type
useStore._pinia = pinia
}
}
const store: StoreGeneric = pinia._s.get(id)!
if (__DEV__ && hot) {
const hotId = '__hot:' + id
const newStore = isSetupStore
? createSetupStore(hotId, setup, options, pinia, true)
: createOptionsStore(hotId, assign({}, options) as any, pinia, true)
hot._hotUpdate(newStore)
// cleanup the state properties and the store from the cache
delete pinia.state.value[hotId]
pinia._s.delete(hotId)
}
// save stores in instances to access them devtools
if (
__DEV__ &&
IS_CLIENT &&
currentInstance &&
currentInstance.proxy &&
// avoid adding stores that are just built for hot module replacement
!hot
) {
const vm = currentInstance.proxy
const cache = '_pStores' in vm ? vm._pStores! : (vm._pStores = {})
cache[id] = store
}
// StoreGeneric cannot be casted towards Store
return store as any
}
useStore.$id = id
return useStore
}
此函数首先根据传参格式,获取唯一标识id和相关配置options,然后返回useStore函数。useStore函数最终会返回store,它首先通过getCurrentInstance方法获取vue实例,紧接着去判断Pinia是否存在,不存在的话通过inject(piniaSymbol)获取。然后去设置当前活跃的Pinia实例。通过setUp是不是函数去判断应该走createSetupStore函数还是createOptionsStore函数。下面主要分析createOptionsStore函数。
createOptionsStore(方法路径:packages/pinia/src/store.ts 105行)
function createOptionsStore<
Id extends string,
S extends StateTree,
G extends _GettersTree<S>,
A extends _ActionsTree
>(
id: Id,
options: DefineStoreOptions<Id, S, G, A>,
pinia: Pinia,
hot?: boolean
): Store<Id, S, G, A> {
const { state, actions, getters } = options
const initialState: StateTree | undefined = pinia.state.value[id]
let store: Store<Id, S, G, A>
function setup() {
if (!initialState && (!__DEV__ || !hot)) {
/* istanbul ignore if */
if (isVue2) {
set(pinia.state.value, id, state ? state() : {})
} else {
pinia.state.value[id] = state ? state() : {}
}
}
// avoid creating a state in pinia.state.value
// 主要是把传入的state变成响应式
const localState =
__DEV__ && hot
? // use ref() to unwrap refs inside state TODO: check if this is still necessary
toRefs(ref(state ? state() : {}).value)
: toRefs(pinia.state.value[id])
return assign(
localState,
actions,
// 把getters的value值由原本的普通函数转成计算属性
Object.keys(getters || {}).reduce((computedGetters, name) => {
computedGetters[name] = markRaw(
computed(() => {
setActivePinia(pinia)
// it was created just before
const store = pinia._s.get(id)!
// allow cross using stores
/* istanbul ignore next */
if (isVue2 && !store._r) return
// @ts-expect-error
// return getters![name].call(context, context)
// TODO: avoid reading the getter while assigning with a global variable
// 返回回调函数中带有store参数,因此使用的时候可以getters:{test: (state) => state + 1}
return getters![name].call(store, store)
})
)
return computedGetters
}, {} as Record<string, ComputedRef>)
)
}
store = createSetupStore(id, setup, options, pinia, hot)
store.$reset = function $reset() {
const newState = state ? state() : {}
// we use a patch to group all changes into one single subscription
this.$patch(($state) => {
assign($state, newState)
})
}
return store as any
}
首先通过setUp函数去整合state和getters同时转换成响应式数据,然后通过assign()方法去整合actions,最终一并返回。然后通过createSetupStore函数去返回store。
createSetupStore核心逻辑
定义了partialStore变量去整合_p、$id、$onAction、$patch、$subscribe(callback, options = {})、$dispose相关内容。
然后去整合store挂载到Pinia上
const store: Store<Id, S, G, A> = reactive(
assign(
__DEV__ && IS_CLIENT
? // devtools custom properties
{
_customProperties: markRaw(new Set<string>()),
_hmrPayload,
}
: {},
partialStore
// must be added later
// setupStore
)
) as unknown as Store<Id, S, G, A>
// store the partial store now so the setup of stores can instantiate each other before they are finished without
// creating infinite loops.
// 挂载到pinia上
pinia._s.set($id, store)
最后useStore函数返回store
const store: StoreGeneric = pinia._s.get(id)! // pinia._s.get(id)值就是上面pinia._s.set($id, store)设置的
return store as any