Skip to content

前言

手写 Pinia:从 0 到 1 构建一个生产级 Vue 状态管理库

第一阶段:重新理解 Pinia 的本质

  • Pinia 不是 Vuex 5,它是 Vue3 响应式系统的“第一公民” apps/web-docs/column/Pinia/Part01.md 基于 reactiverefcomputed,无 Mutation/Module 的复杂抽象
  • Composition API + 响应式:状态即 reactive 对象,计算即 computed apps/web-docs/column/Pinia/Part02.md 零学习成本,与 Vue3 开发模式无缝融合
  • TypeScript 优先:类型自动推导,无需冗余声明 apps/web-docs/column/Pinia/Part03.md defineStore 返回类型自动包含 stategettersactions
  • 模块化设计: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() 函数返回的 statereactive 包裹
    实现响应式追踪
  • 热更新:__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 之间执行,避免依赖收集
  • 性能对比:$patch vs 多次 state.x = y

$reset 重置状态

  • $reset() 如何将 state 恢复到初始值?
    调用 setup() 重新生成初始状态,Object.assignstore.$state
  • 注意:$reset 不会重置 actionsgetters 的引用

第三阶段:掌握 actions 与方法系统

actions 的实现

  • actions 本质:定义在 setup() 内部的函数,通过 proxy 挂载到 store
    为什么 actions 能访问 this?—— this 被绑定为 store 实例
  • this 绑定机制:在 defineStore 内部,actions 函数被 bind(store)
    避免箭头函数(无 this)导致无法访问 state
  • 异步 actionsasync/await 自然支持,返回 Promise
    await store.fetchUser()
  • $onAction 钩子:如何拦截 action 的执行、成功、失败?
    用于日志、监控、错误处理
  • 类型安全:actions 的参数与返回类型如何推导?
    映射类型 ActionsTree + 函数重载

第四阶段:穿透 getters 与派生状态

getters 的实现

  • getters 本质:computed 的封装
    getters: { fullName: (state) => state.firstName + state.lastName }
  • this 指向:在 gettersthis 指向 store,可访问其他 getters
    实现方式:computed(() => getter.call(store))
  • 缓存机制:computed 的惰性求值与脏检查
    依赖的 state 不变,则不重新计算
  • 类型推导:getters 如何推导返回类型?
    GettersTree 映射类型,this 类型为 Store 自身

第五阶段:生产级插件系统与扩展

pinia.use() 插件机制

  • 插件的本质:(context: PiniaPluginContext) => Partial<Store>
    可扩展 stategettersactions、自定义属性
  • PiniaPluginContext 包含:piniastoreapp(Vue 实例)、options
    提供上下文信息
  • 常见插件场景:
    • 持久化(pinia-plugin-persistedstate
    • 日志(devtools
    • 拦截 action 执行
  • 实现:在 store 创建后,遍历所有插件并合并返回值

持久化插件手写实现

  • localStorage 读写:store.$state 序列化与反序列化
  • hydrate(注水):从 localStorage 恢复状态
  • dehydration(脱水):将 state 保存到 localStorage
  • 时机:store 初始化后 hydrate$patchdehydration
  • 类型安全:如何确保存储的数据与 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 如何推导 idstategettersactions 类型?
    使用 UnionToIntersectionExtractStore 等高级类型
  • Tree-shaking 友好:每个 store 独立,未引用的 store 不会打包