mini-pinia
Pinia 源码
effectScope
它的主要作用是创建一个可以独立管理响应式副作用(effect)的作用域。即可以控制某些响应式数据的开始或停止
在 Vue 中,”副作用” 通常指由 watch、watchEffect 或组件的渲染函数所产生的响应式依赖关系。这些副作用会自动跟踪其依赖,并在依赖变化时重新执行。
effectScope 的核心价值在于:
- 集中管理:将一组相关的副作用(比如多个
watch)收集到一个单独的 “作用域” 中。 - 统一清理:可以通过调用作用域的
stop()方法,一次性停止该作用域内的所有副作用。这对于清理工作非常有用,尤其是在组件卸载时。
1 | import { effectScope, watch, ref } from 'vue'; |
plugin
在 Vue 3 中,插件(Plugin)是扩展 Vue 功能的重要方式,可用于添加全局功能(如全局组件、指令、原型方法、状态管理等),或封装复用逻辑。插件的设计旨在增强 Vue 应用的灵活性和可扩展性,常见的如 vue-router、pinia 等都是典型的 Vue 插件。
基本概念
Vue 3 插件本质上是一个包含 install 方法的对象,或一个返回该对象的函数。当插件被安装时,Vue 会自动调用其 install 方法,并传入 app(当前应用实例)和可选的配置参数。
插件可以实现的功能包括:
- 注册全局组件、指令(如
app.component()、app.directive()); - 扩展应用实例(如添加全局方法或属性,通过
app.config.globalProperties); - 提供全局资源(如注入全局依赖,通过
app.provide()); - 集成第三方库(如将
axios封装为插件,方便全局使用)。
插件的使用步骤
使用插件的核心流程是:定义插件 → 安装插件 → 在应用中使用插件提供的功能。
- 定义插件
插件必须包含 install 方法,该方法接收两个参数:
app:当前的 Vue 应用实例(createApp创建的实例);options:安装插件时传入的配置参数(可选)。
以下是几个常见的插件定义示例:
注册全局组件 / 指令
1 | // myPlugin.js |
扩展全局属性 / 方法
通过 app.config.globalProperties 可以给所有组件添加全局属性或方法(类似 Vue 2 中的 Vue.prototype)。
1 | // httpPlugin.js(封装 axios 为例) |
提供全局依赖(依赖注入)
通过 app.provide() 提供全局依赖,组件可通过 inject 接收,适合跨组件共享数据 / 方法。
1 | // themePlugin.js |
- 安装插件
在创建 Vue 应用实例后,通过 app.use(plugin, options) 安装插件。use 方法会自动调用插件的 install 方法,并传入 app 和 options。
1 | // main.js |
- 在应用中使用插件功能
插件安装后,其提供的全局功能可在组件中直接使用:
1 | <!-- App.vue --> |
其他用法
插件链式调用
app.use() 方法返回应用实例本身,因此可以链式调用多个插件:
1 | app.use(plugin1).use(plugin2, { option: "xxx" }).use(plugin3).mount("#app"); |
函数式插件
插件也可以是一个返回安装对象的函数,更灵活地处理参数:
1 | // 函数式插件 |
Pinia 原理
首先我们需要了解 pinia 的三个核心概念
- Pinia 实例:整个状态管理的「根容器」,全局唯一,负责管理所有 Store。
- Store:独立的状态模块(比如用户模块
userStore、购物车模块cartStore),包含状态(state)、计算属性(getters)、方法(actions)。 - State:Store 中存储的原始响应式数据(类似组件的
data),是状态管理的核心。
它们之间的关系是:
Pinia 中管理着各种的 store 其中每一个 store 都是一个独立的状态板块 这些独立板块 store 中储存着各自的 state,getters,actions,而 state 是其中的一个部分,用来储存其中的原始响应式数据
我们一般的使用步骤可能是这样的
在 main.js 中
1 | import { createPinia } from "pinia"; |
在某个 id 下的 store 中
1 | import {defineStore} from 'pinia' |
打印一下 pinia

其中 store 是一个 Map 储存键值对 进一步地 即储存了 id 和对应的 options(选项式) 或者是对应的 setup (组合式)
state 是目前储存了的原始响应式数据
另外 返回了一个 effectScope 的作用域 用于控制一部分的响应式数据的开始与消失
install 注册了全局的 pinia 我们在其中返回 pinia 的相关内容(即下面的东西)
同时还有各种自带的 api 会在后面一一实现
工具函数
封装一些简单的工具函数
参数处理
1 | export function formaArgs(args) { |
这里可以判断传递参数的类型 因为当我们传递参数的时候可能有这样两种情况
当使用 setup 方式传参时
1 | export const useUserStore = defineStore("user", () => {}); |
可以看到 传递的第二个参数是一个函数
当使用 options 方式传参时
1 | export const useStore = defineStore('user'.{ |
传递的第二个参数不是函数
我们通过这样的特征来判断使用的是哪种方式
判断类型
1 | export function isComputed(value) { |
computed 中有一个自带的元素 effect 是用来判断 computed 的一大特征
createPinia
1 | import { piniaSymbol } from "./global.js"; |
需要暴露的即是上面说到的几个关键部分
defineStore
在 defineStore 中 主体上我们需要返回一个 useStore 方法
定义 Pinia Store 的核心入口函数(对应官方 Pinia 的 defineStore API)
- 作用:接收用户配置,返回一个用于获取/创建 Store 实例的函数(useStore)
- @param {…any} args - 用户传入的参数,支持两种格式:
- 组合式写法:defineStore(‘storeId’, () => { … })
- 选项式写法:defineStore(‘storeId’, { state, getters, actions })
- @returns {Function} useStore - 供组件调用的 Store 实例获取函数
在写的时候觉得为什么这里返回的是一个方法 而不是直接返回实例呢
- 延迟创建:把 Store 的创建时机从「模块加载时」推迟到「组件调用时」,确保 Pinia 实例已安装。
- 单例共享:通过
pinia.store.has(id)检查,确保全应用只有一个该 id 的 Store 实例,实现状态共享。 - 适配 Vue 机制:在组件中调用
useStore时,inject才能拿到 Pinia 实例(因为 inject 只能在组件中调用),同时适配effectScope等响应式作用域管理。
进一步地 在这个方法中我们需要实现什么功能呢?
1 | export default function defineStore(...args) { |
然后我们分别处理 setup 与 option 的 store 创建
createSetupStore
在其中 我们要创建对应的 store 并且编译 setup 中的内容
1 | function createSetupStore(id, setup, pinia) { |
compileSetup
我们将纯响应式对象挂载到 pinia 实例中 储存到 state 里面
1 | function compileSetup(pinia, id, setupStore) { |
这里的挂载方式是 state 的 value 中是一个键值对 我们给这个键值对赋值 从而储存好 state
createOptionsStore
1 | function createOptionsStore(id, pinia, options) { |
基本逻辑是跟 setup 部分一致的 这里就不再写了 主要是在 compile 中有区别
compileOptions
1 | function compileOptions(pinia, store, id, { state, getters, actions }) { |
我们需要逐个去分析其中的 state getter action 目的是要将 option 里单独的部分最终转换为 setup 其中的形式 例如 getters 中的我们最终要换成 computed
createStoreState
1 | function createStoreState(pinia, id, state) { |
在 option 中 state 是返回的一个函数 所以如果当前 state 存在的话 我们直接执行这个 state 就能拿到值
当然 最后我们需要把它重新变为响应式 (参考 setup 中的写法)
createStoreGetters
1 | function createStoreGetters(store, getters) { |
这里的 reduce 是 JavaScript 数组的迭代累加方法,核心作用是:遍历数组的每一项,把它们 “合并” 成一个最终结果(对象 / 数字 / 数组等)。
reduce 的两个关键参数
1 | array.reduce(回调函数, 初始值) |
第一个参数:回调函数(每次遍历数组项时执行)
回调函数里又有两个参数:
wrapper:「累加器」,每次循环都会把上一次的结果传给它(这里是 “正在构建的新 getters 对象”);getterName:「当前项」,每次遍历到的数组元素(这里是getters对象的键名,比如doubleCount)。
第二个参数:初始值(
reduce开始执行时,wrapper的初始状态),这里是{}(空对象)。
这段代码里的 reduce,本质是「遍历 getters 的所有键名,把每个 getter 函数包装成 computed 后,组装成一个新对象返回」。
createStoreActions
1 | function createStoreActions(store, actions) { |
最基础的部分实际上就是
1 | for (let actionName in actions) { |
即我们挨个执行一下 actionList 中的函数 其他部分是用来实现后面的 subscribe api 这里先按下不表
setStore
这是最终对 store 的整合
1 | function setStore(pinia, store, id, result, state) { |
$patch
store.$patch 是一个高效批量更新状态的 API,核心作用是:一次性修改多个 state 属性,或通过函数形式灵活更新状态,且仅触发一次响应式更新(性能更优)。
执行中 我们可以直接返回一个键值对对象 也可以返回一个函数 用来修改状态
1 | userStore.$patch({ |
所以 这里我们实现 patch 的主要逻辑是
1.当返回的是对象的时候 我们直接把这个新对象与原对象合并
2.当返回的是函数的时候 我们直接把它需要的 state 给传递进去然后执行函数即可
1 | export const createPatch = (pinia, id) => { |
然后我们在 utils 中封装一下合并对象的函数 这里不能直接用自带的 Object.assign 因为 state 中也可能含有 对象 我们需要进行深合并 防止某些属性丢失 所以我们需要逐个合并 并且进行递归
递归的思路是 如果新值和旧值都是对象的话,我们就继续合并 当一方不是会员的时候 我们直接赋值
1 | export function mergeObject(newVals, oldVals) { |
最后我们在 defineStore 中 封装一下 createApi 用来给 store 赋上这些 api
1 | function createApis(pinia, id, scope) { |
我们只需要在创建 store 的时候调用一下这个方法即可(setup 同理)
在 createOptionsStore 与 createSetupStore 中
1 | store = reactive(createApis(pinia, id, storeScope)); |
需要注意的是
**这里我们实现 api 的时候都是外层包裹函数用来接受参数 里面返回原 api 用的是闭包 **
$reset
核心作用是:将 Store 的 state 一键恢复到「初始定义时的状态」(比如 state 函数返回的原始值),无需手动逐个重置属性,简洁高效。
需要注意的是 这个方法只针对 options 写法 组合式的话我们只需要在里面自定义一个函数用来重置状态即可 比如
1 | // 手动定义 $reset 逻辑(必须显式写) |
而在我们的 options 写法中 由于 state 本身就是一个函数 返回的是一个对象 并且这个对象就是元素原始值 故而我们只需要拿到最开始的对象 然后进行一次合并即可
1 | export function createReset(store, stateFn) { |
需要注意的是
$reset 的底层实现(createReset(store, state))需要两个核心参数,这两个参数只有在 setStore 执行时才同时就绪:
store实例:已经是被 Pinia 包装后的响应式对象(前面通过reactive(createApis(...))创建),具备了基础的 Store 结构;state函数:Store 定义时的原始state函数(用于生成初始状态快照,是$reset能 “重置” 的核心依据)。
所以我们直接在挂载 store 的时候引入这个方法
1 | function setStore(pinia, store, id, result,state) { |
$subscribe
subscribe 是 监听 Store 状态变化 的核心方法,用于响应 Store 中状态的修改(包括直接修改、通过 action 修改、$patch 修改等),支持全局监听或局部监听,还能精准捕获变化细节(如变化的状态键、旧值、新值)。
在 pinia 的使用中
1 | // 监听 Store 所有状态变化 |
使用的时候会执行其中的回调函数
mutation 指向变化细节对象,那么其实就是
1 | store.$subscribe(({ storeId }, state) => {}); |
所以在我们实现的时候我们需要传入一个回调函数,另外我们还需要传入 options 用来作为监听参数
1 | function $subscribe(callback,options={}) |
在其中我们对变化的部分做监听即可
另外,我们还需要能使得 subscribe 能够受 scope 的管理 固然在外面再用 scope 包裹一下即可
1 | export function createSubscribe(pinia, id, scope) { |
我们拿到 id 监听对应部分 然后在 callback 中传入需要的两个部分即可
调用方式与 patch 相同
$onAction
在 Pinia 中,onAction 是一个核心订阅方法,用于监听 Store 中所有 Action 的执行过程(包括同步 / 异步 Action),支持在 Action 执行前、执行后、出错时触发回调,还能获取 Action 的名称、参数、返回值等关键信息。它是 Pinia 提供的 “副作用监听” 能力,常用于日志记录、埋点统计、数据校验、权限拦截等场景。
在实际使用中
1 | const clg = store.$onAction(({ after, onError }) => { |
核心需求是:
- 允许外部注册回调(
cb),监听 Action 的执行过程; - 回调能拿到
after(成功后触发)和onError(失败后触发)两个方法,用于注册后续逻辑; - 同步 Action 和异步 Action(返回 Promise)都要兼容;
- 遵循「订阅 - 触发」模式(先收集回调,再在对应时机执行)。
我们需要实现的大致逻辑是
保存回调函数 -> 发布订阅 -> 创建容器 -> 执行回调函数 -> 在 actions 中触发 -> actions 中创建列表逐个执行
首先我们封装一个工具函数用来收集回调函数 并且设定触发函数 用来控制回调函数的触发
1 | export const subscription = { |
在 api 中 声明一个全局的 actionList 用来存放 action
1 | export let actionList = []; |
我们需要在执行解析 action 的环节添加新的逻辑用来处理 after 与 onerror 的逻辑
1 | function createStoreActions(store, actions) { |
1 | subscription.triggle(actionList, { after, onError }); |
- 这是「Action 执行前」的核心步骤:把
actionList中所有外部注册的回调(通过$onAction传入的)全部触发; - 给每个回调传入
{ after, onError }两个方法 —— 这正是 PiniaonAction回调的核心参数,让外部能通过这两个方法注册「成功后」和「失败后」的逻辑; - 这里的
after/onError是当前 Action 专属的(步骤 3 定义的局部函数),外部调用after(cb)时,会把回调存入当前 Action 的afterList,确保回调只对当前 Action 生效。
$dispose
在 Pinia 中,dispose 是 Store 实例的核心生命周期方法,用于手动销毁 Store 并清理其关联资源(如订阅、监听器、副作用等),本质是触发 Store 的「卸载逻辑」,让 Store 从 Pinia 实例中移除并释放内存。
实现 dispose 我们需要实现下面几个功能
1.关闭当前的作用域 scope
2.将 actionList 重置为空
3.将 store 中的映射根据 id 删除掉
4.将 id 最底层储存的 state 也删除掉
1 | export function createDispose(pinia, id, scope) { |
需要注意的地方是 store 中储存的是 map 键值对 当我们删除后 只是删除了这个 map 中的映射,其实在底层的 state 中这个值仍然存在 那么就不能实现我们期望的功能 所以我们应该去最底层的删除掉 pinia.state.value[id] 才能完全卸载掉
调用方式仍然是放在 createApis 中返回
$state
store.$state 本身不存储任何数据,只是「读取」全局状态中当前 Store 对应的命名空间(pinia.state.value[id]),相当于一个「快捷访问入口」。
为什么不直接在
store上存储状态?Pinia 的设计是「全局状态集中管理」,所有 Store 的状态都存在
pinia.state.value中(单一数据源),Store 实例本身只是「状态的访问器」和「逻辑容器」(包含actions``getters)。如果每个 Store 自己存一份状态,会导致「全局状态与 Store 状态不一致」的问题。
1 | /** |
plugin 实现
在 Pinia 中,Plugin(插件) 是扩展 Store 功能的核心机制,允许你在不侵入 Store 定义的前提下,批量添加全局逻辑(如状态持久化、日志记录、权限校验等)。插件本质是一个函数,接收 context 上下文参数(包含 Pinia 实例、Store 实例等核心信息),可通过它修改 Store 行为、添加属性 / 方法、监听状态变化等。
Pinia 插件的工作流程:
- 插件是一个函数,接收
context对象(包含pinia、app、store等关键信息); - 插件在 Store 初始化(第一次调用
useXXXStore())时执行; - 可通过
context.store访问 / 修改当前 Store 实例(如添加属性、监听状态、扩展方法); - 支持返回一个对象,对象中的属性 / 方法会被合并到 Store 实例上(简化扩展逻辑);
- 多个插件按注册顺序执行,形成链式扩展。
Pinia 插件的核心流程是「注册插件 → 存储插件 → 执行插件」
注册插件与储存插件
首先我们需要给 pinia 上挂载一个 plugins 数组
1 | export default function createPinia() { |
提供 use 方法(插件注册入口);
- 用
plugins数组(插件存储容器)保存所有注册的插件 (即 cb push 进数组); - 返回包含
plugins数组的 Pinia 实例(即 this),让后续逻辑能访问到已注册的插件。
执行插件
在 defineStore 中 我们封装一个执行插件的函数
1 | function runPlugins(pinia, store) { |
核心作用是:在 Store 初始化时,批量执行所有已注册的插件,并将插件返回的扩展属性合并到 Store 实例上,最终实现「插件扩展 Store 功能」的核心目标。
传入 store 调用插件函数并且传入上下文对象(这里是简化过的 包含 store 的对象 在官方源码中可以传入更完整的 context 包括 pinia app options 等)
最后如果有 res 我们直接把返回的对象合并到 Store 实例上 这样就可以将插件返回的扩展属性合并到 Store 实例上
设计意图:通过「对象合并」实现插件对 Store 的「无痛扩展」,无需侵入 Store 内部定义,符合 Pinia 插件的「非侵入式」设计理念。
官方源码及解析
官方源码 :
源码解析:
[🍍 深入解析 Pinia 源码:一份全面的指南 Pinia 是 Vue 的存储库,它允许您跨组件/页面共享状态。 如果您 - 掘金]:
我们主要要从中 熟悉 pinia 的使用 ,熟练原生 js 的实现 ,理解 pinia 的设计原理 。








