pinia源码学习心得
首次读源码,无论体量大小,略感吃力。
ai盛行,能力也越来越强大,让ai为我们赋能是一件刻不容缓的事情。
首先让ai帮我生成了一份学习计划
prompt如下:
我是一个前端开发工程师,想要学习pinia项目源码,请分析项目结构,告诉我该如何进行学习
cursor生成的学习计划很详细,可用性也挺高。
pinia^1^
首先先谈一下,粗略阅读后,对pinia的理解吧
- pinia允许创建多个pinia实例,因此会有
activcePinia
来保存当前激活的pinia实例。在createPinia
后,会将新创建的pinia实例
进行激活。 - 每个pinia都使用
effectScope
进行了作用域隔离,便于管理响应式数据,以及dispose
后的数据清理 - pinia下允许创建
store
,definestore
的所有store
均在pinia._s
存储了映射关系,并将 store 内的 state 数据合并到了 pinia 的state 上。 - 每个 store 也进行了
effectScope
作用域隔离,用于单个store的响应式数据管理。 - store 的创建支持options和setup两种形式,而
createOptionsStore
的作用其实是把 option 包装一层setup后,调用createSetupStore
,因此createSetupStore
是核心函数 - pinia 层次清晰的进行了pinia实例、store的作用域隔离清晰,便于管理。
- store 的数据更新,采用发布订阅机制来完成,维护了
subscriptions
数组,存储$subscribe
订阅的回调函数,$patch
触发更新时,遍历并执行subscriptions
的回调。其中$patch
兼容了 {} 配置以及callback回调两种形式,对于配置式调用,将 store 的 state 进行合并,而callback回调的形式则是选择了将当前store的state传入后执行。 action
的作用,执行我们 reurn 的函数,并遍历执行$onAction
加入的回调- 我们可以在
$onAction
添加回调,会被收集到actionSubscriptions
,而在action
调用时会遍历执行,我们使用options编码时,action
指向明确,自然会被收集,而setup
式编写时,createOptionsStore
是依据我们return出的函数进行调用action
,因此,我们如果想要监听definStore内我们一些异步函数,并进行后续同步操作的话,需要将该函数放入到return中。

下面记录一下,源码阅读历程
pinia架构

Pinia 是一个 monorepo 项目,采用 pnpm workspace 管理:
packages/pinia/
- 核心状态管理库 ⭐packages/testing/
- 测试工具包packages/nuxt/
- Nuxt.js 集成packages/docs/
- 文档站点packages/playground/
- 演示示例scripts/
- 构建和发布脚本
pinia整体基于pnpm workspace实现monorepe架构策略,将pinia及其周边产品进行统一的仓库管理。
核心api实现

createpinia
创建pinia实例,
ts
function createPinia(): Pinia {
const scope = effectScope(true)
// NOTE: here we could check the window object for a state and directly set it
// if there is anything like it with Vue 3 SSR
const state = scope.run<Ref<Record<string, StateTree>>>(() =>
ref<Record<string, StateTree>>({})
)!
let _p: Pinia['_p'] = []
// plugins added before calling app.use(pinia)
let toBeInstalled: PiniaPlugin[] = []
const pinia: Pinia = markRaw({
install(app: App) {
// this allows calling useStore() outside of a component setup after
// installing pinia's plugin
setActivePinia(pinia)
pinia._a = app
app.provide(piniaSymbol, pinia)
app.config.globalProperties.$pinia = pinia
/* istanbul ignore else */
if (__USE_DEVTOOLS__ && IS_CLIENT) {
registerPiniaDevtools(app, pinia)
}
toBeInstalled.forEach((plugin) => _p.push(plugin))
toBeInstalled = []
},
use(plugin) {
if (!this._a) {
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. Avoid old browsers like IE11.
if (__USE_DEVTOOLS__ && IS_CLIENT && typeof Proxy !== 'undefined') {
pinia.use(devtoolsPlugin)
}
return pinia
}
effectScope
使用vue的effectScope
api进行顶层作用域的隔离,方便在dispose
之后,对响应式数据的清理
install
pinia是作为插件/工具库的形式出现的,为了贴合vue的插件集成机制,createPinia方法暴露出install,用于vue对pinia实例的全局挂载。
ts
install(app: App) {
// this allows calling useStore() outside of a component setup after
// installing pinia's plugin
setActivePinia(pinia)
pinia._a = app
app.provide(piniaSymbol, pinia)
app.config.globalProperties.$pinia = pinia
/* istanbul ignore else */
if (__USE_DEVTOOLS__ && IS_CLIENT) {
registerPiniaDevtools(app, pinia)
}
toBeInstalled.forEach((plugin) => _p.push(plugin))
toBeInstalled = []
}
use
pinia提供了一套插件系统集成的机制,用于扩展pinia的功能,eg:持久化工具pinia-plugin-persistedstat
用户所添加的插件可能发生在,vue挂载pinia之前或之后完成,因此进行兼容性处理,
ts
use(plugin) {
if (!this._a) {
toBeInstalled.push(plugin)
} else {
_p.push(plugin)
}
return this
}
defineStore
使用TS重载约定了两种使用方式
ts
export function defineStore<
Id extends string,
S extends StateTree = {},
G extends _GettersTree<S> = {},
// cannot extends ActionsTree because we loose the typings
A /* extends ActionsTree */ = {},
>(
id: Id,
options: Omit<DefineStoreOptions<Id, S, G, A>, 'id'>
): StoreDefinition<Id, S, G, A>
/**
* Creates a `useStore` function that retrieves the store instance
*
* @param id - id of the store (must be unique)
* @param storeSetup - function that defines the store
* @param options - extra options
*/
export function defineStore<Id extends string, SS>(
id: Id,
storeSetup: (helpers: SetupStoreHelpers) => SS,
options?: DefineSetupStoreOptions<
Id,
_ExtractStateFromSetupStore<SS>,
_ExtractGettersFromSetupStore<SS>,
_ExtractActionsFromSetupStore<SS>
>
): StoreDefinition<
Id,
_ExtractStateFromSetupStore<SS>,
_ExtractGettersFromSetupStore<SS>,
_ExtractActionsFromSetupStore<SS>
>
做了参数兼容,判断第二位参数传入了setup,则使用第三个传参作为optipn
ts
function defineStore(
// TODO: add proper types from above
id: any,
setup?: any,
setupOptions?: any
): StoreDefinition {
let options:
| DefineStoreOptions<
string,
StateTree,
_GettersTree<StateTree>,
_ActionsTree
>
| DefineSetupStoreOptions<
string,
StateTree,
_GettersTree<StateTree>,
_ActionsTree
>
const isSetupStore = typeof setup === 'function'
// the option store setup will contain the actual options in this case
options = isSetupStore ? setupOptions : setup
function useStore(pinia?: Pinia | null, hot?: StoreGeneric): StoreGeneric {
...
}
}
useStore
先确保当前的pinia的激活
ts
if (pinia) setActivePinia(pinia)
判断当前 id 是否已经创建了store
ts
if (!pinia._s.has(id)) {
// creating the store registers it in `pinia._s`
if (isSetupStore) {
// 函数式调用的store
createSetupStore(id, setup, options, pinia)
} else {
// 对象式调用的store
createOptionsStore(id, options as any, pinia)
}
/* istanbul ignore else */
if (__DEV__) {
// @ts-expect-error: not the right inferred type
useStore._pinia = pinia
}
}
开发环境下的热更新机制
_hotUpdate
的实现后续补充
ts
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)
}
createOptionsStore
ts
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 */
pinia.state.value[id] = state ? state() : {}
}
// avoid creating a state in pinia.state.value
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,
Object.keys(getters || {}).reduce(
(computedGetters, name) => {
if (__DEV__ && name in localState) {
console.warn(
`[🍍]: A getter cannot have the same name as another state property. Rename one of them. Found with "${name}" in store "${id}".`
)
}
computedGetters[name] = markRaw(
computed(() => {
setActivePinia(pinia)
// it was created just before
const store = pinia._s.get(id)!
// allow cross using stores
// @ts-expect-error
// return getters![name].call(context, context)
// TODO: avoid reading the getter while assigning with a global variable
return getters![name].call(store, store)
})
)
return computedGetters
},
{} as Record<string, ComputedRef>
)
)
}
store = createSetupStore(id, setup, options, pinia, hot, true)
return store as any
}
createOptionsStore
的只是将options
的选项解耦处理后,包装成了包含 return 的setup方法,然后调用createSetupStore
createSetupStore
同 createPinia
一样,使用effectScope
进行store的作用域隔离。
createSetupStore
的作用就是创建store
,将store
挂载到pinia._s
上,最后将其 return
ts
function createSetupStore<
Id extends string,
SS extends Record<any, unknown>,
S extends StateTree,
G extends Record<string, _Method>,
A extends _ActionsTree,
>(
$id: Id,
setup: (helpers: SetupStoreHelpers) => SS,
options:
| DefineSetupStoreOptions<Id, S, G, A>
| DefineStoreOptions<Id, S, G, A> = {},
pinia: Pinia,
hot?: boolean,
isOptionsStore?: boolean
): Store<Id, S, G, A> {
...
const partialStore = {
_p: pinia,
// _s: scope,
$id,
$onAction: addSubscription.bind(null, actionSubscriptions),
$patch,
$reset,
$subscribe(callback, options = {}) {
const removeSubscription = addSubscription(
subscriptions,
callback,
options.detached,
() => stopWatcher()
)
const stopWatcher = scope.run(() =>
watch(
() => pinia.state.value[$id] as UnwrapRef<S>,
(state) => {
if (options.flush === 'sync' ? isSyncListening : isListening) {
callback(
{
storeId: $id,
type: MutationType.direct,
events: debuggerEvents as DebuggerEvent,
},
state
)
}
},
assign({}, $subscribeOptions, options)
)
)!
return removeSubscription
},
$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
// must be added later
// setupStore
)
: partialStore
) as unknown as Store<Id, S, G, A>
// 将 store 挂载
pinia._s.set($id, store as Store)
...
return store
}
$subscribe
向subscriptions
添加回调函数,用于$patch
执行时触发。
ts
function $subscribe(callback, options = {}) {
const removeSubscription = addSubscription(
subscriptions,
callback,
options.detached,
() => stopWatcher()
)
const stopWatcher = scope.run(() =>
watch(
() => pinia.state.value[$id] as UnwrapRef<S>,
(state) => {
if (options.flush === 'sync' ? isSyncListening : isListening) {
callback(
{
storeId: $id,
type: MutationType.direct,
events: debuggerEvents as DebuggerEvent,
},
state
)
}
},
assign({}, $subscribeOptions, options)
)
)!
return removeSubscription
}
addSubscription
的作用就是向subscriptions
pushcallback
,并返回一个移除该callback
的回调,当detached
为 true 时,内部也使用了onScopeDispose
监听EffectScope
(作用域)被销毁时自动执行清理操作
ts
function addSubscription<T extends _Method>(
subscriptions: T[],
callback: T,
detached?: boolean,
onCleanup: () => void = noop
) {
subscriptions.push(callback)
const removeSubscription = () => {
const idx = subscriptions.indexOf(callback)
if (idx > -1) {
subscriptions.splice(idx, 1)
onCleanup()
}
}
if (!detached && getCurrentScope()) {
onScopeDispose(removeSubscription)
}
return removeSubscription
}
$patch
支持传入函数、对象
ts
function $patch(stateMutation: (state: UnwrapRef<S>) => void): void
function $patch(partialState: _DeepPartial<UnwrapRef<S>>): void
如果传入回调,则执行回调;
如果传入对象,则进行state合并;
然后遍历触发subscriptions
,而subscriptions
是$subscribe
添加的
ts
function $patch(
partialStateOrMutator:
| _DeepPartial<UnwrapRef<S>>
| ((state: UnwrapRef<S>) => void)
): void {
let subscriptionMutation: SubscriptionCallbackMutation<S>
isListening = isSyncListening = false
// reset the debugger events since patches are sync
/* istanbul ignore else */
if (__DEV__) {
debuggerEvents = []
}
if (typeof partialStateOrMutator === 'function') {
// 如果传入回调,则执行回调;
partialStateOrMutator(pinia.state.value[$id] as UnwrapRef<S>)
subscriptionMutation = {
type: MutationType.patchFunction,
storeId: $id,
events: debuggerEvents as DebuggerEvent[],
}
} else {
// 如果传入对象,则进行state合并
mergeReactiveObjects(pinia.state.value[$id], partialStateOrMutator)
subscriptionMutation = {
type: MutationType.patchObject,
payload: partialStateOrMutator,
storeId: $id,
events: debuggerEvents as DebuggerEvent[],
}
}
const myListenerId = (activeListener = Symbol())
nextTick().then(() => {
if (activeListener === myListenerId) {
isListening = true
}
})
isSyncListening = true
// because we paused the watcher, we need to manually call the subscriptions
// 执行 $subscribe 添加的的回调
triggerSubscriptions(
subscriptions,
subscriptionMutation,
pinia.state.value[$id] as UnwrapRef<S>
)
}
mergeReactiveObjects
这里贴出代码的目的是,由于
mergeReactiveObjects
是通过遍历赋值实现更新,而不是全覆盖,所以明确当我们使用对象式的$patch
时,如果需要属性置空,也是要进行一个空值的属性赋值
tsfunction mergeReactiveObjects< T extends Record<any, unknown> | Map<unknown, unknown> | Set<unknown>, >(target: T, patchToApply: _DeepPartial<T>): T { // Handle Map instances if (target instanceof Map && patchToApply instanceof Map) { patchToApply.forEach((value, key) => target.set(key, value)) } else if (target instanceof Set && patchToApply instanceof Set) { // Handle Set instances patchToApply.forEach(target.add, target) } // no need to go through symbols because they cannot be serialized anyway for (const key in patchToApply) { if (!patchToApply.hasOwnProperty(key)) continue const subPatch = patchToApply[key] const targetValue = target[key] if ( isPlainObject(targetValue) && isPlainObject(subPatch) && target.hasOwnProperty(key) && !isRef(subPatch) && !isReactive(subPatch) ) { // NOTE: here I wanted to warn about inconsistent types but it's not possible because in setup stores one might // start the value of a property as a certain type e.g. a Map, and then for some reason, during SSR, change that // to `undefined`. When trying to hydrate, we want to override the Map with `undefined`. target[key] = mergeReactiveObjects(targetValue, subPatch) } else { // @ts-expect-error: subPatch is a valid value target[key] = subPatch } } return target }
Footnotes
pinia
我来帮你分析 Pinia 项目的结构,并制定一个学习源码的路径。让我先探索项目的整体结构。
🍍 Pinia 项目源码学习指南
根据我对 Pinia 项目结构的分析,这是一个非常好的状态管理库源码学习项目。以下是我为你制定的学习路径:
📁 项目结构概览
Pinia 是一个 monorepo 项目,采用 pnpm workspace 管理:
packages/pinia/
- 核心状态管理库 ⭐packages/testing/
- 测试工具包packages/nuxt/
- Nuxt.js 集成packages/docs/
- 文档站点packages/playground/
- 演示示例scripts/
- 构建和发布脚本
📚 推荐学习路径
🎯 第一阶段:理解核心概念 (1-2天)
-
从 README.md 开始
- 了解 Pinia 的设计理念和特点
- 理解为什么它是 Vuex 的继承者
-
查看基本使用示例
12:15:packages/playground- 先运行 playground 示例 - 理解 defineStore、createPinia 的基本用法
🔧 第二阶段:核心源码剖析 (3-5天)
按以下顺序阅读核心文件:
-
入口文件 -
packages/pinia/src/index.ts
- 了解整体 API 导出结构
- 掌握核心概念的命名
-
Pinia 实例创建 -
packages/pinia/src/createPinia.ts
typescript- createPinia() 函数实现 - effectScope 的使用 - 插件系统设计
-
根状态管理 -
packages/pinia/src/rootStore.ts
typescript- 全局状态管理机制 - setActivePinia/getActivePinia - 插件接口定义
-
核心存储实现 -
packages/pinia/src/store.ts
⭐⭐⭐typescript- defineStore 函数 (行 837-948) - createOptionsStore vs createSetupStore - Store 的生命周期管理
-
类型定义 -
packages/pinia/src/types.ts
typescript- TypeScript 类型系统设计 - Store、State、Getters、Actions 类型定义
🧪 第三阶段:测试和实践 (2-3天)
-
阅读测试用例 -
packages/pinia/__tests__/
12:15:packages/pinia/__tests__/store.spec.ts- 理解 API 的预期行为 - 学习最佳实践
-
辅助功能
storeToRefs.ts
- 响应式引用转换mapHelpers.ts
- Vue 2 兼容的辅助函数subscriptions.ts
- 订阅机制
🛠 第四阶段:高级特性 (2-3天)
- 开发工具集成 -
packages/pinia/src/devtools/
- 热重载支持 -
packages/pinia/src/hmr.ts
- SSR 支持 - 查看 SSR 相关测试
- 插件生态 -
packages/testing/
和packages/nuxt/
🎯 学习建议
📖 阅读顺序
- 广度优先: 先理解整体架构
- 深度优先 : 重点研究
store.ts
文件 - 实践导向: 结合测试用例理解 API
🔍 关键技术点
Vue 3 特性应用:
- Composition API
- effectScope
- 响应式系统 (reactive, ref, computed)
- 依赖注入 (provide/inject)
TypeScript 高级特性:
- 泛型约束
- 条件类型
- 工具类型
- 类型推断
设计模式:
- 单例模式 (Store 实例管理)
- 插件模式 (PiniaPlugin)
- 观察者模式 (订阅机制)
🛠 实践练习
-
克隆并运行项目
bashgit clone https://github.com/vuejs/pinia.git cd pinia pnpm install pnpm dev # 运行测试
-
调试核心函数
- 在
defineStore
中加 console.log - 观察 Store 创建过程
- 在
-
编写简单插件
- 实现一个状态持久化插件
- 理解插件机制
💡 学习成果验证
完成学习后,你应该能够:
- 📝 解释 Pinia 的核心架构设计
- 🔧 实现 自定义 Store 和插件
- 🎯 优化 Vue 应用的状态管理
- 🐛 调试 Pinia 相关问题
- 📚 贡献 开源代码
📚 额外资源
这个学习路径大约需要 1-2 周时间,可以根据你的 经验水平调整节奏。重点是理解 Vue 3 的响应式系统和 TypeScript 的类型系统在状态管理中的应用!
↩