前言

pinia作为全局的状态管理器,可帮助我们在vue项目中跨多个组件间进行通讯,支持vue2以及vue3,我们可通过操作state、action,就像是在一SFC文件中使用一般,
本文将从pinia的使用,到源码层面来分析关于pinia的工作过程,加深对pinia的理解,以及在pinia的实现过程上学习到了什么等等!!

pinia的使用

  1. 首先在应用程序入口中将pinia引入到vue项目中
    1
    2
    3
    4
    5
    6
    7
    8
    9
    import { createApp } from 'vue'
    import { createPinia } from 'pinia'
    import App from './App.vue'

    const pinia = createPinia()
    const app = createApp(App)

    app.use(pinia)
    app.mount('#app')
  2. 根据业务场景,定义自己的模块store
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
      // stores/counter.js
    import { defineStore } from 'pinia'
    export const useCounterStore = defineStore('counter', {
    state: () => {
    return { count: 0 }
    },
    // 也可以这样定义
    // state: () => ({ count: 0 })
    actions: {
    increment() {
    this.count++
    },
    },
    })
  3. 直接在组件中使用
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    <template>
    <!-- 直接从 store 中访问 state -->
    <div>Current Count: {{ counter.count }}</div>
    </template>
    <script setup>
    import { useCounterStore } from '@/stores/counter'
    const counter = useCounterStore()
    counter.count++
    // 自动补全! ✨
    counter.$patch({ count: counter.count + 1 })
    // 或使用 action 代替
    counter.increment()
    </script>
    这样子之后,我们就可以在vue上下文中直接使用store.state以及store.actions了!
    😕 那么,这个pinia在执行的过程中,发生了什么呢?为什么能够像在SFC文件中简单使用,就能够实现跨组件级别的共享呢?下面将一一道来!

pinia使用过程分析

使用createPinia()创建一vue插件,池子的形成

一切从方法源头开始:const pinia = createPinia(),通过这个createPinia()方法,创建一个pinia插件,该插件Pinia对象的组成结构如下:

1
2
3
4
5
6
7
8
9
// rootStore.ts
export interface Pinia{
state: Ref<Record<string, StateTree>> // 整颗状态树
use(plugin: PiniaPlugin): Pinia // 注册Pinia插件函数的动作
_p: PiniaPlugin[] // 已通过use()方法注册的Pinia插件
_a: App //应用实例
_e: EffectScope
_s: Map<string, StoreGeneric> // 以键值对进行存储的Store对象
}

通过对这个Pinia对象的结构进行分析,我们可以得知,在应用程序入口这里所创建出来的pinia,仅仅只是一个“池子”的概念,也就是将store容器给创建好,将全局的state状态树给维护好,等待后续的使用,这个过程如下所示:

1
2
3
4
5
6
7
8
9
10
export const piniaSymbol = (
__DEV__ ? Symbol('pinia') : /* istanbul ignore next */ Symbol()
) as InjectionKey<Pinia>
const pinia: Pinia = markRaw({
install(app: App){
// 这里隐藏其他代码
app.provide(piniaSymbol, pinia)
app.config.globalProperties.$pinia = pinia
}
})

:+1: 从上述代码这里我们看出,将在全局范围内注入一个pinia对象,也就是说,我们可以在vue上下文中通过enject的方式来获取,因为这里将对依赖的访问通过一个公共的piniaSymbol来进行访问了,因此,我们也有理由相信,当使用的useStore()的方式来创建/获取到一个store对象的时候,也是通过这个方式来获取的!

根据所需创建子store,丰富池子内容

👉 借助于defineStore()创建一个store对象,也就是创建一个useStore()函数,这里是一个高阶函数,返回一个函数的函数,因此,当我们通过defineStore()来定义一个store对象的时候,其实就是创建了一个store函数的时候,这里仅仅是捆绑id并创建了一个store函数,还并没有真正调用到这个store函数, 👉 当调用了const counter = useCounterStore()的时候,才是真正执行了这个store函数!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
export function defineStore(id){
function useStore(pinia?: Pinia | null, hot?: StoreGeneric): StoreGeneric {
// _s即为Map<string, Store>的容器,这里将id与store进行关联,并存储于pinia中的_s对象中
if(!pinia._s.has(id)){
// 这里采用懒加载的方式来创建这个store
if (isSetupStore) {
createSetupStore(id, setup, options, pinia)
} else {
//这里选项式也将最终调用createSetupStore,将选项式参数组装为组合式所需的对象,也就是将state进行ref化、getters进行computed化、与actions合并成为一个大的对象,这个过程,我原称之为“脱壳响应化”,脱去state、getters、actions外壳,将其中的数据ref化
createOptionsStore(id, options as any, pinia)
}
}
const store: StoreGeneric = pinia._s.get(id)!
return store
}
return useStore
}

🌟 上述在创建store的时候,会将其他相关的api一同集成到store对象中,切将store给reactive化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
const partialStore = {
_p: pinia,
$id,
$onAction: addSubscription.bind(null, actionSubscriptions),
$patch,
$reset,
$subscribe(callback, options = {}) {},
$dispose,
} as _StoreWithState<Id, S, G, A>
const store: Store<Id, S, G, A> = reactive(
__DEV__ || (__USE_DEVTOOLS__ && IS_CLIENT)
? assign(
{
_hmrPayload,
_customProperties: markRaw(new Set<string>()), // devtools custom properties
},
partialStore
)
: partialStore
) as unknown as Store<Id, S, G, A>
// 然后将store给存储下来
pinia._s.set($id, store as Store)

上述关于store的创建,也同时说明了关于其他成员属性的由来,以及各自都是用来做什么操作的!
🌠 这样子之后,我们便能够在后续的操作中,通过提供的id来从pinia._s中获取到已经初始化完成的store对象!
👉 每一个store的创建过程其实就是将stategettersactions给“脱壳响应化”后,追加上公共的属性/API的过程!!

学到点什么

  1. 在基于整套框架层面进行的编码时,可以借鉴这个pinia实现provide以及enject的方式,如果我们需要在整个app层面,提供能够在各个组件直接访问的资源时,可通过自定义统一的symbol()属性的方式,让所有的访问者都通过一个唯一的标识来获取!
  2. 在定义vue插件的时候,可采用懒加载的方式,先提供一个容器,容器的外观可以先定义出来,然后在实际需要使用到的时候,才进行初始化并进行存储操作!
  3. 使用选项式的方式与组合式的方式来使用pinia,其结果都是一样的,无非就是使用了选项式的时候,需要经过程序自动转换的一个过程!