Pinia 源码解析与实现原理
约 3542 字大约 12 分钟
布欧-Lewyon
2026-05-15
首页 › Vue › 路由与状态管理 › Pinia 源码解析与实现原理
Pinia 从设计到实现只用了约 1KB(gzip) 的代码,却完整替代了 Vuex 的所有能力。理解它的源码可以学到"小而精"的库设计哲学。
总体架构
| 模块 | 职责 | Pinia 源码文件 |
|---|---|---|
createPinia | 创建 Pinia 实例,Vue 插件 | src/createPinia.ts |
defineStore | 定义并注册 Store | src/store.ts |
storeToRefs | 提取响应式引用 | src/storeToRefs.ts |
types.ts | TypeScript 类型定义 | src/types.ts |
subscribe.ts | $subscribe 实现 | src/subscribe.ts |
设计模式分析
| 设计模式 | Pinia 中的应用 | 作用 |
|---|---|---|
| Singleton | 每个 Store ID 对应一个实例 | 同一 ID 的 Store 全局唯一,复用 reactive state |
| Factory | defineStore(id, options/fn) | 根据传入参数类型创建 OptionStore 或 SetupStore |
| Plugin | app.use(pinia) + pinia.use(plugin) | 注入全局能力 + 插件机制扩展 Store |
| Proxy | $patch 的自动派发 | 批量修改 state 并优化触发时机 |
| Observer | Vue reactive + computed | 状态变化自动通知所有组件 |
| Active Record | Store 实例封装状态+方法 | 通过 this 直接访问 state/getters/actions |
Plugin 模式:Pinia 如何安装
createPinia——工厂函数
// 简化版 src/createPinia.ts
import { ref, markRaw, effectScope, toRaw } from 'vue'
export function createPinia() {
// 1. Pinia 实例核心
const pinia = {
// 唯一标识(用于 provide 注入)
_s: new Map(), // Store 缓存 Map:id → Store 实例
_e: null, // effectScope(统一管理所有 Store 的 effect)
_p: [], // 插件列表
// ⭐ 根状态:所有 store 的 state 都挂在此对象上
// pinia.state.value.counter = { count: 0 }
// pinia.state.value.user = { name: 'Alice' }
state: ref({}),
// 注册插件
use(plugin) {
this._p.push(plugin)
return this
},
// 🔌 插件安装(app.use 时由 install 调用的内部方法)
install(app) {
// 将 pinia 实例 provide 给所有子组件
app.provide(piniaSymbol, pinia)
// 注入 $pinia(Options API 支持)
app.config.globalProperties.$pinia = pinia
// ⭐ 在根组件上注册 effectScope
// 这样当 app.unmount() 时,所有 Store 的 effect 自动清理
pinia._e = effectScope(true)
pinia._e.run(() => {
// effectScope 创建的 scope 在组件卸载时自动停止全部 effect
})
// 标记 Pinia 已安装(防止重复安装)
pinia._a = app
// 使用 markRaw 标记,避免 pinia 实例本身被转为响应式代理
markRaw(pinia)
}
}
return pinia
}install 的核心步骤
- provide pinia 实例 → 子组件通过
inject(piniaSymbol)获取 - 注入
$pinia→ Options API 中通过this.$pinia访问 - 创建 effectScope → 统一管理所有 Store 的响应式依赖,应用卸载时自动清理
- 标记
markRaw→ 防止 pinia 实例本身被 Vue 转换为响应式(它只需要管理 state,不需要自身响应式)
根状态设计(State Tree)
Pinia 将所有 Store 的状态集中管理在 pinia.state 上:
// pinia.state = ref({})
// 类似 Vuex 的单一状态树
// 注册 counter store 后:
pinia.state.value = {
counter: { count: 0, name: '计数器' }
}
// 注册 user store 后:
pinia.state.value = {
counter: { count: 0, name: '计数器' },
user: { id: null, name: '' }
}这种设计的优势:
- SSR 支持:序列化整个
pinia.state.value即可拿到所有状态 - DevTools 集成:只需监控一个 ref 对象
- Hydration:服务端渲染后,客户端注入
pinia.state.value完成水合
Factory + Singleton 模式:defineStore
defineStore 是 Pinia 对外暴露的唯一入口。它同时是 Factory(根据参数创建不同类型 Store)和 Singleton(同一 ID 只创建一个实例)。
// 简化版 src/store.ts
import { getCurrentInstance, inject, effectScope, reactive, computed, toRef, toRaw } from 'vue'
function defineStore(idOrOptions, storeSetup) {
// 1. 归一化参数
// defineStore('counter', { ... }) → Option Store
// defineStore('counter', () => { ... }) → Setup Store
// defineStore({ id: 'counter', ... }) → 对象形式
const { id, options } = normalize(idOrOptions, storeSetup)
// 2. 返回一个 useStore 函数
// 每次调用 useStore() 时,获取 pinia 实例并创建/获取 Store
const useStore = (pinia = null, hotContext) => {
// app.currentApp 仅在安装时存在,用于获取 pinia 实例
const currentInstance = getCurrentInstance()
pinia = pinia || (currentInstance && inject(piniaSymbol))
// ⭐ 关键:没有 pinia 实例时直接报错(install 了吗?)
if (!pinia) {
throw new Error('[@pinia]: getActivePinia was called with no active Pinia.')
}
// ⭐ Singleton:检查缓存
// 同一个 pinia._s Map 中最多只有一个 id 对应的 Store
if (pinia._s.has(id)) {
return pinia._s.get(id)
}
// 创建 Store(区分 Option Store 和 Setup Store)
if (isSetupStore(options)) {
createSetupStore(id, options, pinia, hotContext)
} else {
createOptionsStore(id, options, pinia, hotContext)
}
// 获取刚创建的 Store
const store = pinia._s.get(id)
// 运行所有已注册的插件
pinia._p.forEach(plugin => {
// 插件接收 { pinia, app, store, options }
const result = plugin({ pinia, app: pinia._a, store, options })
// 插件返回的对象浅合并到 store 上
Object.assign(store, result)
})
// 返回 Store
return store
}
useStore.$id = id
return useStore
}缓存机制的依赖图
Option Store 的创建
// 简化版 src/store.ts
function createOptionsStore(id, options, pinia) {
const { state, getters, actions } = options
// 1. ⭐ 初始化 state
// 将 state() 的返回值注册到根状态树
function setup() {
// 注意:pinia.state.value[id] 此时可能已被提前设置
// (用于 SSR hydration)
const localState = pinia.state.value[id] || state()
pinia.state.value[id] = localState
// 将 state 属性转为 ref(利用 Vue 响应式)
// 直接使用 toRefs 让每个属性独立响应式
const refs = {}
for (const key in localState) {
refs[key] = toRef(pinia.state.value[id], key)
}
// ⭐ Option Store 的 setup 返回值
// 包含:state 的 ref + getters 的 computed + actions 函数
return {
...refs, // state → refs
...Object.keys(getters).reduce((acc, key) => {
// getters → computed
acc[key] = computed(() => {
// getter 中的 this 指向 store 实例
const store = pinia._s.get(id)
return getters[key].call(store, store)
})
return acc
}, {}),
...actions, // actions → 原样合并
}
}
// 2. 委托给 createSetupStore
// Option Store 的本质是:用 state/getters/actions 生成一个 setup 函数
// 然后作为 Setup Store 处理
const store = createSetupStore(id, setup, pinia)
}与 Setup Store 的关系
关键洞察:Option Store 只是 Setup Store 的一种语法糖。内部把
state转为ref,getters转为computed,actions原样保留,然后作为 Setup Store 创建。
Setup Store 的创建
// 简化版 src/store.ts
function createSetupStore($id, setup, pinia, isOptionStore) {
// 1. 创建 Store Shell(外壳对象)
const store = reactive({}) // Store 本身是一个 reactive 对象!
// 2. 在 effectScope 中执行 setup
// effectScope 确保 Store 卸载时所有 effect 自动清理
let scope
const setupResult = pinia._e.run(() => {
scope = effectScope()
return scope.run(() => setup())
})
// 3. 将 setup 返回值展开到 store 上
// ref → 自动解包(reactive 特性)
// computed → 作为属性
// 函数 → 绑定 this 为 store
for (const key in setupResult) {
const value = setupResult[key]
if (typeof value === 'function') {
// ⭐ 函数绑定 store 作为 this
// 这样 actions 中 this.count 指向 store 实例
store[key] = value.bind(store)
} else {
// ref/computed/raw value
store[key] = value
}
}
// 4. 注册到缓存
pinia._s.set($id, store)
// 5. ⭐ 自定义 $ 方法
// $patch / $subscribe / $reset / $dispose 等
store.$patch = function(partialStateOrMutator) {
// ...
}
store.$subscribe = function(callback, options) {
// ...
}
store.$reset = function() {
// ...
}
return store
}reactive Store 的奥秘
Store 本身就是 reactive
const store = reactive({})
// 当 setup 返回的 ref 被赋值到 store 上时,
// reactive 会自动解包 ref → store.count === count.value
// 所以组件中可以直接 store.count 而不是 store.count.valuesetup 返回值展开规则
const store = reactive({})
// ref → 自动解包
store.count = ref(0) // store.count === 0(解包了)
store.count++ // ✅ 直接操作
// computed → 解包为计算属性值
store.double = computed(...) // store.double === 计算结果
// 函数 → 绑定 this 为 store
store.increment = function() { this.count++ }Option Store 中的 this 指向
// Option Store 中 this 指向 store 实例
const store = defineStore('counter', {
state: () => ({ count: 0 }),
getters: {
double(state) {
// 接收 state 作为第一个参数(推荐,方便类型推导)
// 也可以通过 this.count 访问
return state.count * 2
}
},
actions: {
increment() {
// ⭐ this = store 实例
this.count++
this.double // 也可以访问 getter
}
}
})actions 中 this 的实现:
// 在 createSetupStore 中,函数会被 bind(store)
for (const key in setupResult) {
if (typeof setupResult[key] === 'function') {
store[key] = setupResult[key].bind(store)
}
}$patch——代理模式
function $patch(partialStateOrMutator) {
// 方式一:传入对象 → 浅合并
if (typeof partialStateOrMutator === 'object') {
// 使用 Object.assign 浅合并
Object.assign(this, partialStateOrMutator)
}
// 方式二:传入函数 → 获取原始 state 进行操作
else if (typeof partialStateOrMutator === 'function') {
// 调用 mutator(state)
// 注意:传入的是 toRaw(this) 而非响应式代理
// 这样内部操作不触发中间更新,只触发一次
partialStateOrMutator(toRaw(this))
}
// ⭐ 批处理:修改完成后,Pinia 内部调用一次 trigger
// 但 Pinia 本身不手动 trigger——依赖 Vue 的批量更新机制
// Vue 会将同一个 Tick 内的多次状态变更合并为一次更新
}// $patch 用法
store.$patch({ count: store.count + 1 })
// 批量操作(只触发一次更新)
store.$patch((state) => {
state.count++
state.name = 'New Name'
state.items.push({ id: 1 })
})批处理原理:Vue 的响应式系统在同一个 Tick(微任务)内多次修改会合并为一次更新。
$patch的 mutator 函数中所有操作都在同步代码块内完成,自然被 Vue 批量处理。
$subscribe——观察者模式
function $subscribe(callback, options = {}) {
// 使用 Vue 的 watch 监听整个 store
// 注意:options.detached 控制 unsubscribe 时机
const stop = watch(
pinia.state.value[$id], // 监听根状态的对应部分
(state, oldState) => {
callback(
{ storeId: $id, type: MutationType.direct },
state,
oldState
)
},
{ flush: 'sync', ...options } // sync 确保每次修改都通知
)
// 返回取消订阅函数
return stop
}自动持久化的实现
利用 $subscribe 可以轻松实现状态持久化:
const store = useCounterStore()
store.$subscribe((mutation, state) => {
localStorage.setItem('counter', JSON.stringify(state))
})
// Pinia 插件方式(全局持久化)
function persistPlugin({ store, options }) {
if (options.persist) {
// 恢复
const saved = localStorage.getItem(store.$id)
if (saved) store.$patch(JSON.parse(saved))
// 订阅
store.$subscribe((_, state) => {
localStorage.setItem(store.$id, JSON.stringify(state))
})
}
}$reset——状态重置
function $reset() {
// 获取初始 state
const initialState = options.state()
// 遍历初始状态,依次设置到 store
for (const key in initialState) {
store[key] = initialState[key]
}
}注意:reset仅在∗∗OptionStore∗∗可用(‘reset
的定义需要访问options.state` 获取初始值)。Setup Store 需要自行实现 reset 逻辑。
storeToRefs——响应式解构
// 简化版 src/storeToRefs.ts
export function storeToRefs(store) {
// 关键:只提取 ref 和 computed 属性,跳过函数
const refs = {}
for (const key in store) {
const value = store[key]
// 跳过函数(actions)
if (typeof value === 'function') continue
// 如果是 ref 或 reactive → 转为 ref
if (isRef(value) || isReactive(value)) {
refs[key] = toRef(store, key)
}
}
return refs
}为什么不能直接用 toRefs?
const store = useCounterStore()
// ❌ toRefs(store) 会将函数也转为 ref → 报错
const { count, double, increment } = toRefs(store)
// increment 是函数,转为 ref 后 value 是函数,类型错误
// ✅ storeToRefs 只处理 ref/computed,跳过函数
const { count, double } = storeToRefs(store)
const { increment } = store // actions 直接解构插件机制详解
// pinia.use(plugin)
const plugin = ({ pinia, app, store, options }) => {
// pinia : Pinia 实例
// app : Vue 应用实例
// store : 刚创建的 Store 实例
// options: defineStore 的第二个参数(原始配置)
// 返回的对象会被浅合并到 store 上
return {
$sharedState: ref('global'),
customMethod() { /* ... */ }
}
}
// 插件的执行时机:每个 useStore 首次被调用时
// 此时 store 已创建,但尚未返回给调用方插件的应用场景:
- 自动持久化 localStorage
- 注入共享状态(当前用户、主题)
- 添加自定义方法(api、toast)
- 数据埋点与日志
DevTools 集成
Pinia 提供了与 Vue DevTools 的深度集成:
// Pinia 在 dev 模式下向 DevTools 发送状态变更信息
function devtoolsSubscribe(store) {
if (__DEV__ && typeof window !== 'undefined') {
// 通过 window.__VUE_DEVTOOLS_GLOBAL_HOOK__ 通信
const hook = window.__VUE_DEVTOOLS_GLOBAL_HOOK__
hook.emit('pinia:state-changed', {
storeId: store.$id,
state: toRaw(store.$state)
})
}
}
// DevTools 面板特性:
// - Timeline:查看状态变更历史
// - Pinia 专属标签:列出所有 Store 及当前状态
// - 时间旅行:回溯到某个历史状态
// - 直接修改:在 DevTools 面板中编辑 state完整流程图
与 Vuex 架构对比
| 对比维度 | Vuex | Pinia |
|---|---|---|
| 状态存储 | store.state 单一状态树 | pinia.state.value 按 id 分片 |
| 修改状态 | commit mutation → mutation 同步函数 | 直接修改 state / $patch |
| 异步操作 | dispatch action → action 异步 | actions 原生支持 async |
| 模块化 | modules 嵌套(命名空间) | 每个 defineStore 自动隔离 |
| 类型安全 | 需额外类型声明 | 自动类型推导 |
| 插件 | 只能全局 | 每个 Store 独立触发 plugin |
| 体积 | ~10KB | ~1KB(Tree-shaking 友好) |
| 源码复杂度 | 多层抽象(Module Collection / Module / Store) | 单一文件核心,简洁直接 |
实现一个最小版 Pinia
为了加深理解,实现一个核心功能的迷你版本:
// mini-pinia.ts
import { ref, computed, reactive, toRef, watch, effectScope, getCurrentInstance, inject } from 'vue'
const piniaSymbol = Symbol('pinia')
// 1. createPinia
export function createPinia() {
const pinia = {
_s: new Map(),
_e: null,
_p: [],
state: ref({}),
use(plugin) { this._p.push(plugin); return this },
install(app) {
app.provide(piniaSymbol, pinia)
app.config.globalProperties.$pinia = pinia
pinia._e = effectScope(true)
}
}
return pinia
}
// 2. defineStore
export function defineStore(id, options) {
const useStore = () => {
const pinia = inject(piniaSymbol)
if (!pinia) throw new Error('No Pinia instance')
if (pinia._s.has(id)) return pinia._s.get(id)
// 初始化 state
const state = options.state ? reactive(options.state()) : {}
pinia.state.value[id] = state
// 创建 getters
const getterRefs = {}
if (options.getters) {
for (const key in options.getters) {
getterRefs[key] = computed(() => options.getters[key](state))
}
}
// 创建 store
const store = reactive({
$id: id,
$state: pinia.state.value[id],
...state,
...getterRefs,
})
// 绑定 actions
if (options.actions) {
for (const key in options.actions) {
store[key] = options.actions[key].bind(store)
}
}
// $patch
store.$patch = (mutator) => {
if (typeof mutator === 'object') {
Object.assign(store, mutator)
} else if (typeof mutator === 'function') {
mutator(pinia.state.value[id])
}
}
// $reset
store.$reset = () => {
const initial = options.state()
for (const key in initial) {
store[key] = initial[key]
}
}
pinia._s.set(id, store)
return store
}
useStore.$id = id
return useStore
}
// 3. storeToRefs
export function storeToRefs(store) {
const refs = {}
for (const key in store) {
if (typeof store[key] !== 'function') {
refs[key] = toRef(store, key)
}
}
return refs
}小结
| 设计模式 | Pinia 实现 |
|---|---|
| Factory | createPinia() + defineStore() |
| Singleton | pinia._s Map 缓存 Store 实例 |
| Observer | Vue reactive + computed + watch |
| Plugin | pinia.use() + 插件运行时注入 |
| Proxy | $patch 对批量修改的代理封装 |
| Active Record | Store 实例封装 state + getters + actions |
| 机制 | 一句话实现原理 |
|---|---|
| Store 实例 | 通过 reactive({}) 创建,setup 返回值展开到其上 |
| 缓存 | pinia._s: Map<string, Store>,首次创建后永久缓存 |
| Option Store | 归一化为 setup 函数(state→ref, getters→computed, actions→bind) |
| $patch | 对象:Object.assign;函数:传入 toRaw(this) 批量操作 |
| $subscribe | watch(pinia.state.value[id], callback) |
| storeToRefs | 过滤函数后,toRef(store, key) 提取 ref |
| 插件 | 每个 Store 首次创建时遍历 _p 数组执行,返回值合并到 store |
