手写 Mini Pinia:100 行代码搞懂 Vue 状态管理本质
理解一个状态管理库最好的方式就是手写一个。本文用约 100 行 TypeScript 实现一个 Mini Pinia,涵盖 defineStore、响应式 state、getter、action、$patch、$subscribe 等核心功能。读完本文你会对 Pinia 的内部机制有底气清晰的直觉。
目标功能
- ✅
defineStore支持 Setup Store 写法 - ✅ 响应式 state(
ref)和 getter(computed) - ✅ action(普通函数,可异步)
- ✅
$patch批量修改 - ✅
$subscribe订阅 state 变化 - ✅ 多 store 管理
- ✅ Vue 插件
install
核心实现
import {
ref, reactive, watch, computed, inject, effectScope,
type Ref, type ComputedRef
} from 'vue'
// 全局 pinia 实例
let activePinia: Pinia | null = null
// store 定义函数的类型
type StoreDefinition<S, G, A> = (
state: S,
getters: G,
actions: A
) => void
// Pinia 类
class Pinia {
_s: Map<string, any> = new Map() // store 集合
state: Record<string, any> = reactive({}) // 全局状态对象
use(plugin: (context: any) => void) {
// 插件机制(简化版)
this._plugins = this._plugins || []
this._plugins.push(plugin)
}
_plugins: ((ctx: any) => void)[] = []
install(app: any) {
activePinia = this
app.provide('pinia', this)
app.config.globalProperties.$pinia = this
}
}
function createPinia(): Pinia {
const pinia = new Pinia()
activePinia = pinia
return pinia
}
// defineStore 核心实现
function defineStore<
Id extends string,
S extends Record<string, any>,
G extends Record<string, any>,
A extends Record<string, any>
>(
id: Id,
setup: () => S & G & A
) {
// 返回 useStore 函数
function useStore() {
const pinia = activePinia || inject<Pinia>('pinia')
if (!pinia) {
throw new Error('[Mini Pinia] Pinia 实例未安装')
}
// 如果 store 已存在,直接返回(单例)
if (pinia._s.has(id)) {
return pinia._s.get(id)
}
// 创建 effectScope,确保所有响应式effect在store销毁时清理
const scope = effectScope(true)
let store: any
scope.run(() => {
// 执行 setup 函数,获取 state/getters/actions
const setupResult = setup()
// 遍历 setup 返回值,区分 ref/computed/普通函数
const storeState: Record<string, Ref | ComputedRef> = {}
const storeGetters: Record<string, ComputedRef> = {}
const storeActions: Record<string, Function> = {}
for (const [key, value] of Object.entries(setupResult)) {
if (isRef(value) && !isComputed(value)) {
storeState[key] = value as Ref
} else if (isComputed(value)) {
storeGetters[key] = value as ComputedRef
} else if (typeof value === 'function') {
storeActions[key] = value
}
}
// 将 state 写入全局 pinia.state (用于 DevTools 和 $subscribe)
pinia.state[id] = {}
for (const [key, refValue] of Object.entries(storeState)) {
pinia.state[id][key] = refValue.value
}
// 同步 state ref 和全局 state
watch(
() => pinia.state[id],
(newState) => {
for (const [key, refValue] of Object.entries(storeState)) {
if (refValue.value !== newState[key]) {
refValue.value = newState[key]
}
}
},
{ deep: true }
)
// 构建代理对象
store = reactive({})
// state:通过 getter/setter 代理
for (const [key, refValue] of Object.entries(storeState)) {
Object.defineProperty(store, key, {
get: () => refValue.value,
set: (v: any) => { refValue.value = v },
enumerable: true
})
}
// getters:只读代理
for (const [key, computedValue] of Object.entries(storeGetters)) {
Object.defineProperty(store, key, {
get: () => computedValue.value,
enumerable: true
})
}
// actions:直接挂载,绑定 this
for (const [key, fn] of Object.entries(storeActions)) {
store[key] = fn.bind(store)
}
// $patch 方法
store.$patch = function (patch: Record<string, any> | ((state: any) => void)) {
if (typeof patch === 'function') {
// 函数式 patch
patch(pinia.state[id])
// 同步到 ref
for (const [key, refValue] of Object.entries(storeState)) {
refValue.value = pinia.state[id][key]
}
} else {
// 对象式 patch
for (const [key, value] of Object.entries(patch)) {
if (key in storeState) {
storeState[key].value = value
pinia.state[id][key] = value
} else {
console.warn(`[Mini Pinia] Unknown state key: ${key}`)
}
}
}
}
// $subscribe 方法
store.$subscribe = function (callback: (mutation: any, state: any) => void) {
return watch(
() => pinia.state[id],
(newState, oldState, onCleanup) => {
callback(
{
storeId: id,
type: 'patch', // 简化
payload: newState
},
newState
)
},
{ deep: true }
)
}
// $reset 方法(Setup store 需调用方提供)
store.$reset = function () {
console.warn('[Mini Pinia] $reset needs manual implementation in setup store')
}
// $dispose 方法
store.$dispose = function () {
scope.stop()
pinia._s.delete(id)
delete pinia.state[id]
}
})
// 执行插件
pinia._plugins.forEach(plugin => {
plugin({ store, pinia })
})
// 缓存 store
pinia._s.set(id, store)
return store
}
useStore.$id = id
return useStore
}
辅助函数
function isRef(value: any): value is Ref {
return value !== null && typeof value === 'object' && '__v_isRef' in value
}
function isComputed(value: any): value is ComputedRef {
return isRef(value) && '__v_isReadonly' in value
}
导出 API
export { createPinia, defineStore, setActivePinia }
function setActivePinia(pinia: Pinia) {
activePinia = pinia
}
实际使用
import { ref, computed } from 'vue'
import { createPinia, defineStore } from './mini-pinia'
// 定义 store
const useCounterStore = defineStore('counter', () => {
// state
const count = ref(0)
const name = ref('Counter')
// getters
const double = computed(() => count.value * 2)
const quadruple = computed(() => double.value * 2)
// actions
function increment() {
count.value++
}
function incrementBy(n: number) {
count.value += n
}
async function incrementAsync() {
await new Promise(resolve => setTimeout(resolve, 100))
count.value++
}
function $reset() {
count.value = 0
name.value = 'Counter'
}
return { count, name, double, quadruple, increment, incrementBy, incrementAsync, $reset }
})
// 跨 store 调用
const useLoggerStore = defineStore('logger', () => {
const logs = ref<string[]>([])
function log(message: string) {
logs.value.push(`[${new Date().toISOString()}] ${message}`)
}
function logCount() {
const counter = useCounterStore()
log(`Current count: ${counter.count}`)
}
return { logs, log, logCount }
})
// 应用安装
import { createApp } from 'vue'
const app = createApp({ /* ... */ })
const pinia = createPinia()
app.use(pinia)
// 在组件中使用
const counterStore = useCounterStore()
const loggerStore = useLoggerStore()
// 读取 state
console.log(counterStore.count) // 0
console.log(counterStore.double) // 0
// 修改 state
counterStore.increment()
console.log(counterStore.count) // 1
counterStore.count = 10 // 直接修改
console.log(counterStore.count) // 10
// $patch 批量修改
counterStore.$patch({
count: 100,
name: 'MyCounter'
})
counterStore.$patch((state) => {
state.count += 50
})
console.log(counterStore.count) // 150
// 订阅变化
const unsubscribe = counterStore.$subscribe((mutation, state) => {
console.log(`[${mutation.storeId}] State changed:`, state)
})
counterStore.increment() // 触发订阅
unsubscribe() // 取消订阅
// 跨 store 调用
loggerStore.logCount() // [logger] Current count: 150
在组件中使用
<script setup lang="ts">
import { useCounterStore } from '@/stores/counter'
import { storeToRefs } from './mini-pinia'
const store = useCounterStore()
// 保持响应性解构
const { count, double } = storeToRefs(store)
// actions 直接解构
const { increment } = store
</script>
<template>
<div>
<p>Count: {{ count }}</p>
<p>Double: {{ double }}</p>
<button @click="increment">+1</button>
</div>
</template>
// storeToRefs 实现
import { toRefs, isRef, isReactive } from 'vue'
function storeToRefs(store: any) {
const refs: Record<string, any> = {}
for (const key in store) {
const value = store[key]
if (isRef(value) || isReactive(value)) {
refs[key] = toRef(store, key)
} else {
// computed 也通过 toRef 暴露
refs[key] = toRef(store, key)
}
}
return refs
}
与真实 Pinia 的对比
| 功能 | Mini Pinia | 真实 Pinia |
|---|---|---|
| defineStore (setup) | ✅ | ✅ |
| defineStore (options) | ❌ | ✅ |
| State 响应式 | ✅ ref | ✅ ref/reactive |
| Getters | ✅ computed | ✅ computed |
| Actions | ✅ | ✅ |
| $patch | ✅ | ✅ |
| $subscribe | ✅ | ✅ + events |
| $onAction | ❌ | ✅ |
| $reset | ⚠️ 手动 | ✅ 自动 (options store) |
| $dispose | ✅ | ✅ |
| 插件系统 | ⚠️ 简化版 | ✅ |
| SSR 支持 | ❌ | ✅ |
| DevTools | ❌ | ✅ |
| storeToRefs | ✅ | ✅ |
| HMR 热更新 | ❌ | ✅ |
Pinia 核心设计思想
通过手写我们可以看到 Pinia 的几个关键设计:
1. effectScope:作用域隔离
Pinia 使用 effectScope 来创建 store 的响应式作用域。当 store 被销毁($dispose)时,scope 停止,所有内部的 watch、computed 自动清理,防止内存泄漏。
const scope = effectScope(true)
scope.run(() => {
// 所有 ref/computed/watch 都在这个 scope 内
const count = ref(0)
const double = computed(() => count.value * 2)
watch(count, () => { /* ... */ })
})
// 销毁时自动清理
scope.stop()
2. 单例模式 + 全局注册
每个 store 只创建一次,缓存在 pinia._s Map 中。后续调用 useStore() 返回的是同一个实例。
3. 代理模式暴露 API
store 对象本身是一个 reactive 代理,通过 Object.defineProperty 将 ref/computed/function 统一暴露为 store.xxx。用户不需要关心 .value,直接用 store.count 访问。
这一点非常聪明——它让 store 的使用体验跟普通对象无异,同时保持了内部的响应式追踪。
总结
100 行代码实现的核心逻辑:
- createPinia:创建全局实例,管理所有 store
- defineStore:返回一个
useStore函数,首次调用时执行 setup - effectScope:隔离响应式作用域,支持销毁清理
- 代理对象:统一暴露 ref/getter/action 为
store.xxx - $patch/$subscribe:提供工具方法
理解了这个骨架,再去看 Pinia 源码,就会发现核心就这么简单。Pinia 的优秀之处在于「用最简单的概念组合出最实用的 API」——这正是好的状态管理库该有的样子。
Comments | 0条评论