前言
手写 Pinia:从 0 到 1 构建一个生产级 Vue 状态管理库
第一阶段:重新理解 Pinia 的本质
- Pinia 不是 Vuex 5,它是 Vue3 响应式系统的“第一公民” apps/web-docs/column/Pinia/Part01.md 基于
reactive、ref、computed,无 Mutation/Module 的复杂抽象 - Composition API + 响应式:状态即
reactive对象,计算即computedapps/web-docs/column/Pinia/Part02.md 零学习成本,与 Vue3 开发模式无缝融合 - TypeScript 优先:类型自动推导,无需冗余声明 apps/web-docs/column/Pinia/Part03.md
defineStore返回类型自动包含state、getters、actions - 模块化设计:
defineStore(id, () => {...})支持逻辑拆分 apps/web-docs/column/Pinia/Part04.md 每个 store 是独立的响应式对象 - 轻量核心:无 Mutation、无 Action Payload、无 Module 嵌套 apps/web-docs/column/Pinia/Part05.md 简化心智模型,降低使用成本
第二阶段:深入响应式状态与更新机制
state 的实现
state本质:一个reactive<Record<string, any>>对象
为什么不用ref?因为reactive更适合对象状态- 初始化:
setup()函数返回的state被reactive包裹
实现响应式追踪 - 热更新:
__hotUpdate如何替换state但保留响应式连接?
Vue Devtools 的 HMR 机制 - 类型推导:如何让
store.$state类型自动匹配setup()返回值?
使用泛型S extends () => any
$patch 批量更新
- 为何需要
$patch?避免多次watch触发与视图重渲染
单次更新 vs 多次赋值 - 实现方式一:
Object.assign(store.$state, partialState)
利用reactive的批量更新机制 - 实现方式二:
$patch(fn)接收函数,fn 内部修改state
在pauseTracking/resetTracking之间执行,避免依赖收集 - 性能对比:
$patchvs 多次state.x = y
$reset 重置状态
$reset()如何将state恢复到初始值?
调用setup()重新生成初始状态,Object.assign回store.$state- 注意:
$reset不会重置actions或getters的引用
第三阶段:掌握 actions 与方法系统
actions 的实现
actions本质:定义在setup()内部的函数,通过proxy挂载到store
为什么actions能访问this?——this被绑定为store实例this绑定机制:在defineStore内部,actions函数被bind(store)
避免箭头函数(无this)导致无法访问state- 异步
actions:async/await自然支持,返回Promiseawait store.fetchUser() $onAction钩子:如何拦截action的执行、成功、失败?
用于日志、监控、错误处理- 类型安全:
actions的参数与返回类型如何推导?
映射类型ActionsTree+ 函数重载
第四阶段:穿透 getters 与派生状态
getters 的实现
getters本质:computed的封装getters: { fullName: (state) => state.firstName + state.lastName }this指向:在getters中this指向store,可访问其他getters
实现方式:computed(() => getter.call(store))- 缓存机制:
computed的惰性求值与脏检查
依赖的state不变,则不重新计算 - 类型推导:
getters如何推导返回类型?GettersTree映射类型,this类型为Store自身
第五阶段:生产级插件系统与扩展
pinia.use() 插件机制
- 插件的本质:
(context: PiniaPluginContext) => Partial<Store>
可扩展state、getters、actions、自定义属性 PiniaPluginContext包含:pinia、store、app(Vue 实例)、options
提供上下文信息- 常见插件场景:
- 持久化(
pinia-plugin-persistedstate) - 日志(
devtools) - 拦截
action执行
- 持久化(
- 实现:在
store创建后,遍历所有插件并合并返回值
持久化插件手写实现
localStorage读写:store.$state序列化与反序列化hydrate(注水):从localStorage恢复状态dehydration(脱水):将state保存到localStorage- 时机:
store初始化后hydrate,$patch后dehydration - 类型安全:如何确保存储的数据与
state类型兼容?
第六阶段:SSR 与服务端集成
SSR 支持
- 问题:服务端渲染时,如何避免状态跨请求共享?
store是单例,会导致用户 A 的数据泄露给用户 B - 解决方案:
createPinia()返回的pinia实例必须是请求级别的
在setup()中通过useContext()获取 - 状态脱水(Dehydration):将服务端计算的
state注入 HTML<script>window.__PINIA__ = ${serialize(store.$state)}</script> - 状态注水(Rehydration):客户端启动前,从
window.__PINIA__恢复状态
避免客户端重新请求数据 - 实现:
pinia.state.value = initialState
第七阶段:与 Vue3 生态深度集成
useStore()如何实现?getCurrentInstance()+inject('pinia')
依赖注入获取pinia实例- Devtools 集成:如何向 Vue Devtools 发送
action执行记录?devtools.emit('action-start', payload) - TypeScript 类型增强:
defineStore如何推导id、state、getters、actions类型?
使用UnionToIntersection、ExtractStore等高级类型 - Tree-shaking 友好:每个
store独立,未引用的store不会打包