开始
不得不说,typescript 类型系统对于阅读源码的帮助太大了。能够快速理解函数的输入输出,从而理出整个大致的代码逻辑。
pinia 使用
根据pinia官方用例,pinia 的使用分为两步。
- 创建 store
- 使用 store
其中创建 store 使用 defineStore 函数。使用 store 使用 defineStore 函数返回的 hook 直接获取 store 实例。
defineStore 函数接收两种构建参数。
- setup
- options
setup 就是一个函数,返回具体的 ref 代理对象,options 选项则和 vuex 基本一致。但是也有差异,比如 pinia 没有专用同步选项 mutations,actions 同时支持同步和异步操作。具体差异可以自己查询。
官方示例
// stores/counter.js
import { defineStore } from 'pinia'
export const useCounterStore = defineStore('counter', {
state: () => {
return { count: 0 }
},
// could also be defined as
// state: () => ({ count: 0 })
actions: {
increment() {
this.count++
},
},
})
<script setup>
import { useCounterStore } from '@/stores/counter'
const counter = useCounterStore()
counter.count++
// with autocompletion ✨
counter.$patch({ count: counter.count + 1 })
// or using an action instead
counter.increment()
</script>
<template>
<!-- Access the state directly from the store -->
<div>Current Count: {{ counter.count }}</div>
</template>
defineStore 函数
defineStore 函数接收两种参数组合。
- id + options (或者直接在 options 中传入 id)
- id + setup + setupOptions
defineStore 函数的返回值是一个函数(useStore)。在使用时调用该函数获取 store 实例。
defineStore 函数做了两件事。
- 传入参数的初始化
- 生成 useStore 函数并返回。
参数初始化
传入参数的初始化其实是为了支持函数重载。做的事情非常简单,对 setup 和 options 做判断。这里定义了两个变量,id 和 options。用来缓存传入的 id 和 选项。如果传入的 setup 是函数,则 options 存储的是setupOptions ,否则为 setup。
对应源码:
let id: string
let options
// 判断参数类型
const isSetupStore = typeof setup === 'function'
if (typeof idOrOptions === 'string') {
id = idOrOptions
options = isSetupStore ? setupOptions : setup
} else {
options = idOrOptions
id = idOrOptions.id
}
定义 useStore
useStore 接收两个可选参数,函数返回一个 store。 两个可选参数:
- pinia 实例
- hot 参数,主要用于热重载。pinia 内部处理时传递当前的 store 实例。
执行逻辑
暂时只考虑客户端渲染 Vue 项目的情况下的执行逻辑。
- 检查当前是否存在依赖注入上下文,即 Provide/Inject。
- 通过 inject 获取 pinia 实例。
- 判断 pinia 实例是否存在,存在则执行 setActivePinia,并将获取到的 pinia 实例传入。
- 将 activePinia 的值赋给当前 pinia 实例。
- 判断当前 id 的 store 在当前 pinia 中是否存在。
- 如果不存在则调用对应函数创建新 store(createSetupStore/createOptionsStore)。
- 如果存在直接获取并返回。
以上的执行逻辑存在几个问题。
- 什么时候注入的 pinia 实例?
- setActivePinia 做了什么?
- 如何创建的 store?
注入 pinia 实例
当然可以在调用 useStore 函数的时候手动注入,但是一般在客户端渲染的情况下不需要手动注入(服务端渲染需要调用时创建 pinia 实例并手动注入,保证状态隔离),这是因为在 Vue 项目初始化时,会调用 createPinia 函数创建 pinia 实例并通过 app.use 方法注入。
createPinia
通过 Pinia 接口可以知道 pinia 的结构。
export interface Pinia {
// 给 Vue 使用的安装函数
install: (app: App) => void
// 根 state
state: Ref<Record<string, StateTree>>
// 安装插件
use(plugin: PiniaPlugin): Pinia
// 安装的插件
_p: PiniaPlugin[]
// 当前连接 Vue app
_a: App
// 管理副作用的 Effect Scope。
_e: EffectScope
// store 仓库
_s: Map<string, StoreGeneric>
// 测试标识
_testing?: boolean
}
首先是 install 函数,该函数接收一个参数,即 Vue app,也只做了一件事,执行setActivePinia 函数,参数为当前 pinia 对象。而 setActivePinia 函数会将传入的 pinia 缓存到activePinia 变量上,这样全局就只有一个 pinia。
而整个createPinia 函数做的事情为 初始化 pinia 对象并返回。再通过 app.use 函数注入。
这样就解答了上面两个问题,何时注入 pinia 和 setActivePinia 做了什么?
创建 store
由 defineStore 函数接收的参数其实可以知道,会有两个创建方法,分别对应传入 setup 函数和 options 选项的创建。
但是,最终都会使用 setup 创建,因为 setup 函数的返回值是具体的响应式数据,所以只需要将 options 传入的参数构造为一个 setup 函数再调用 createSetupStore 函数创建 store 即可。
setup 创建
传入 setup 函数的创建方法为 createSetupStore,参数有 4 个,返回一个 store。
参数:
- id 唯一标识
- setup 函数
- options 配置项
- pinia 对应 pinia
执行逻辑
- 在 pinia 上根据传入唯一 id 初始化
pinia.state.value[$id] = {}。 - 初始化 partialStore ,再根据 partialStore 使用 reactive 生成相应时数据 store。
- 将 store 挂载到 pinia._s 上
pinia._s.set($id, store as Store)。 - 执行 setup 函数获取 setupStore。
- 遍历 setupStore 中的数据并处理。
- 合并 store 和 setupStore。
- 为
store对象的$state属性定义 getter 和 setter。 - 执行插件。
- 将 isListening isSyncListening 两个变量置为 true。
- return
结束
这是对 pinia 源代码的简单理解,也是一个阅读记录,如果有啥不对的,欢迎各位大佬指出。