Pinia 源码

effectScope

它的主要作用是创建一个可以独立管理响应式副作用(effect)的作用域。即可以控制某些响应式数据的开始或停止

在 Vue 中,”副作用” 通常指由 watchwatchEffect 或组件的渲染函数所产生的响应式依赖关系。这些副作用会自动跟踪其依赖,并在依赖变化时重新执行。

effectScope 的核心价值在于:

  1. 集中管理:将一组相关的副作用(比如多个 watch)收集到一个单独的 “作用域” 中。
  2. 统一清理:可以通过调用作用域的 stop() 方法,一次性停止该作用域内的所有副作用。这对于清理工作非常有用,尤其是在组件卸载时。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
import { effectScope, watch, ref } from 'vue';

// 1. 创建一个作用域
const scope = effectScope();

const count = ref(0);

// 2. 在作用域内创建副作用
scope.run(() => {
watch(count, (newVal) => {
console.log(`Count changed to: ${newVal}`);
});

//可以在run中创建其子级作用域 父级作用域停止时子级作用域也会停止
const subScope = effectScope() //如果给true参数就是平行作用域

// 你可以在 run 回调中创建多个 effect
watchEffect(() => {
console.log(`Current count is: ${count.value}`);
});
});


count.value++; // 两个副作用都会触发

// 3. 停止作用域内的所有副作用
scope.stop();

count.value++; // 没有任何输出,因为所有副作用都已停止

//可以返回值
const res = scope.run(()=>{
...
return 123
})

console.log(res) //此时res为123

const res = scope.run(() => {
const r = effect(() => {
doubleCount = computed(() => {
return count.value * 2
})
return 123
})
return r()
})

console.log(res) // 此时res也是123

plugin

在 Vue 3 中,插件(Plugin)是扩展 Vue 功能的重要方式,可用于添加全局功能(如全局组件、指令、原型方法、状态管理等),或封装复用逻辑。插件的设计旨在增强 Vue 应用的灵活性和可扩展性,常见的如 vue-routerpinia 等都是典型的 Vue 插件。

基本概念

Vue 3 插件本质上是一个包含 install 方法的对象,或一个返回该对象的函数。当插件被安装时,Vue 会自动调用其 install 方法,并传入 app(当前应用实例)和可选的配置参数。

插件可以实现的功能包括:

  • 注册全局组件、指令(如 app.component()app.directive());
  • 扩展应用实例(如添加全局方法或属性,通过 app.config.globalProperties);
  • 提供全局资源(如注入全局依赖,通过 app.provide());
  • 集成第三方库(如将 axios 封装为插件,方便全局使用)。

插件的使用步骤

使用插件的核心流程是:定义插件安装插件在应用中使用插件提供的功能

  1. 定义插件

插件必须包含 install 方法,该方法接收两个参数:

  • app:当前的 Vue 应用实例(createApp 创建的实例);
  • options:安装插件时传入的配置参数(可选)。

以下是几个常见的插件定义示例:

注册全局组件 / 指令

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// myPlugin.js
export default {
// install 方法会在插件被安装时调用
install(app, options) {
// 注册全局组件
app.component("GlobalButton", {
template: `<button>{{ label }}</button>`,
props: ["label"],
});

// 注册全局指令(如 v-focus)
app.directive("focus", {
mounted(el) {
el.focus(); // 元素挂载后自动聚焦
},
});
},
};

扩展全局属性 / 方法

通过 app.config.globalProperties 可以给所有组件添加全局属性或方法(类似 Vue 2 中的 Vue.prototype)。

1
2
3
4
5
6
7
8
9
10
11
12
// httpPlugin.js(封装 axios 为例)
import axios from "axios";

export default {
install(app, options) {
// 配置 axios 基础路径(通过 options 传入)
axios.defaults.baseURL = options.baseURL || "";

// 添加全局方法 $http,所有组件可通过 this.$http 访问
app.config.globalProperties.$http = axios;
},
};

提供全局依赖(依赖注入)

通过 app.provide() 提供全局依赖,组件可通过 inject 接收,适合跨组件共享数据 / 方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// themePlugin.js
export default {
install(app, options) {
// 提供全局主题配置(key 为 Symbol 更安全,避免命名冲突)
const themeKey = Symbol("theme");
app.provide(themeKey, {
color: options.color || "blue",
setColor: (color) => {
/* 实现主题切换逻辑 */
},
});

// 也可以直接提供给组件注入
app.provide("user", { name: "Vue Plugin" });
},
};
  1. 安装插件

在创建 Vue 应用实例后,通过 app.use(plugin, options) 安装插件。use 方法会自动调用插件的 install 方法,并传入 appoptions

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// main.js
import { createApp } from "vue";
import App from "./App.vue";
import myPlugin from "./myPlugin"; // 自定义插件
import httpPlugin from "./httpPlugin";
import themePlugin from "./themePlugin";

// 创建应用实例
const app = createApp(App);

// 安装插件(可传入配置参数)
app.use(myPlugin); // 无参数
app.use(httpPlugin, { baseURL: "https://api.example.com" }); // 带参数
app.use(themePlugin, { color: "green" });

// 挂载应用
app.mount("#app");
  1. 在应用中使用插件功能

插件安装后,其提供的全局功能可在组件中直接使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
<!-- App.vue -->
<template>
<!-- 使用全局组件 GlobalButton -->
<GlobalButton label="全局按钮" />

<!-- 使用全局指令 v-focus -->
<input v-focus placeholder="自动聚焦" />
</template>

<script setup>
import { getCurrentInstance, inject } from "vue";

// 获取全局属性 $http(在 setup 中需通过 getCurrentInstance)
const instance = getCurrentInstance();
const $http = instance.appContext.config.globalProperties.$http;

// 调用全局方法(如请求接口)
const fetchData = async () => {
const res = await $http.get("/data");
console.log(res);
};

// 注入全局依赖(themePlugin 提供的 user)
const user = inject("user");
console.log(user.name); // 输出:Vue Plugin
</script>

其他用法

插件链式调用

app.use() 方法返回应用实例本身,因此可以链式调用多个插件:

1
app.use(plugin1).use(plugin2, { option: "xxx" }).use(plugin3).mount("#app");

函数式插件

插件也可以是一个返回安装对象的函数,更灵活地处理参数:

1
2
3
4
5
6
7
8
9
10
11
12
// 函数式插件
export default (options) => {
return {
install(app) {
// 可基于 options 动态定义功能
app.config.globalProperties.$custom = options.value;
},
};
};

// 安装时直接传入参数
app.use(pluginFn, { value: "hello" });

Pinia 原理

首先我们需要了解 pinia 的三个核心概念

  1. Pinia 实例:整个状态管理的「根容器」,全局唯一,负责管理所有 Store。
  2. Store:独立的状态模块(比如用户模块 userStore、购物车模块 cartStore),包含状态(state)、计算属性(getters)、方法(actions)。
  3. State:Store 中存储的原始响应式数据(类似组件的 data),是状态管理的核心。

它们之间的关系是:

Pinia 中管理着各种的 store 其中每一个 store 都是一个独立的状态板块 这些独立板块 store 中储存着各自的 state,getters,actions,而 state 是其中的一个部分,用来储存其中的原始响应式数据

我们一般的使用步骤可能是这样的

在 main.js 中

1
2
3
4
5
import { createPinia } from "pinia";

const pinia = createPinia();

app.use(pinia);

在某个 id 下的 store 中

1
2
3
4
5
6
7
8
9
10
11
import {defineStore} from 'pinia'

export const useStore = defineStore('id',{
//如果是组合式
state:()=>{}
getters:{}
actions:{}
//如果是setup
const list = ref({})
const getlist = ()=>{}
})

打印一下 pinia

image-20251111143709433

其中 store 是一个 Map 储存键值对 进一步地 即储存了 id 和对应的 options(选项式) 或者是对应的 setup (组合式)

state 是目前储存了的原始响应式数据

另外 返回了一个 effectScope 的作用域 用于控制一部分的响应式数据的开始与消失

install 注册了全局的 pinia 我们在其中返回 pinia 的相关内容(即下面的东西)

同时还有各种自带的 api 会在后面一一实现

工具函数

封装一些简单的工具函数

参数处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
export function formaArgs(args) {
let id;
let options;
let setup;

if (isString(args[0])) {
id = args[0];
if (isFunction(args[1])) {
setup = args[1];
} else options = args[1];
} else {
options = args[0];
id = args[0].id;
}

return { id, options, setup };
}

export function isString(data) {
return typeof data === "string";
}

export function isFunction(data) {
return typeof data === "function";
}

这里可以判断传递参数的类型 因为当我们传递参数的时候可能有这样两种情况

当使用 setup 方式传参时

1
export const useUserStore = defineStore("user", () => {});

可以看到 传递的第二个参数是一个函数

当使用 options 方式传参时

1
2
3
4
5
export const useStore = defineStore('user'.{
state:()=>{},
getters:{},
actions:{}
})

传递的第二个参数不是函数

我们通过这样的特征来判断使用的是哪种方式

判断类型

1
2
3
4
5
6
7
export function isComputed(value) {
return !!(isRef(value) && value.effect);
}

export function isObject(data) {
return typeof data === "object" && data !== null;
}

computed 中有一个自带的元素 effect 是用来判断 computed 的一大特征

createPinia

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import { piniaSymbol } from "./global.js";
import { ref, effectScope } from "vue";

export default function createPinia() {
const scope = effectScope(true);
const store = new Map();
const state = scope.run(() => ref({}));
function install(app) {
app.provide(piniaSymbol, this);
}

return {
scope,
store,
state,
install,
};
}

需要暴露的即是上面说到的几个关键部分

defineStore

在 defineStore 中 主体上我们需要返回一个 useStore 方法

定义 Pinia Store 的核心入口函数(对应官方 Pinia 的 defineStore API)

  • 作用:接收用户配置,返回一个用于获取/创建 Store 实例的函数(useStore)
  • @param {…any} args - 用户传入的参数,支持两种格式:
    1. 组合式写法:defineStore(‘storeId’, () => { … })
    1. 选项式写法:defineStore(‘storeId’, { state, getters, actions })
  • @returns {Function} useStore - 供组件调用的 Store 实例获取函数

在写的时候觉得为什么这里返回的是一个方法 而不是直接返回实例呢

  1. 延迟创建:把 Store 的创建时机从「模块加载时」推迟到「组件调用时」,确保 Pinia 实例已安装。
  2. 单例共享:通过 pinia.store.has(id) 检查,确保全应用只有一个该 id 的 Store 实例,实现状态共享。
  3. 适配 Vue 机制:在组件中调用 useStore 时,inject 才能拿到 Pinia 实例(因为 inject 只能在组件中调用),同时适配 effectScope 等响应式作用域管理。

进一步地 在这个方法中我们需要实现什么功能呢?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
export default function defineStore(...args) {
// 1. 解析用户传入的参数,统一提取 id(Store 唯一标识)、options(选项式配置)、setup(组合式函数)
const { id, options, setup } = formaArgs(args);

// 2. 判断用户是否使用「组合式写法」(第二个参数是函数则为组合式)
const isSetup = isFunction(setup);

/**
* 核心函数:供组件调用,获取 Store 实例(闭包特性,可访问外层的 id、isSetup 等变量)
* @returns {Object} Store 实例 - 包含状态、计算属性、方法的响应式对象
*/
function useStore() {
// 3. 从 Vue 全局依赖注入中获取 Pinia 根实例(由 createPinia 创建并通过 app.provide 注入)
// 必须在组件上下文调用(如 setup 函数),否则 inject 会返回 undefined
const pinia = inject(piniaSymbol);

// 4. 单例模式:检查当前 id 的 Store 是否已创建(避免重复创建导致状态不共享)
if (!pinia.store.has(id)) {
// 5. 根据写法类型,调用对应的 Store 创建函数
if (isSetup) {
// 组合式写法:通过 createSetupStore 处理 setup 函数返回的响应式状态/方法
createSetupStore(id, pinia, setup);
} else {
// 选项式写法:通过 createOptionsStore 处理 state/getters/actions 配置
createOptionsStore(id, pinia, options);
}
}

// 6. 返回已创建(或缓存)的 Store 实例(确保全应用同一 id 的 Store 唯一)
return pinia.store.get(id);
}

// 7. 返回 useStore 函数,用户通过调用该函数获取 Store 实例(如 const useUserStore = defineStore(...))
return useStore;
}

然后我们分别处理 setup 与 option 的 store 创建

createSetupStore

在其中 我们要创建对应的 store 并且编译 setup 中的内容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
function createSetupStore(id, setup, pinia) {
// 1. 执行用户定义的 setup 函数,获取其返回值(可能包含 ref/reactive/方法等)
const setupStore = setup();

let store; // 最终要返回的 Store 实例(响应式对象)
let storeScope; // 专门管理当前 Store 响应式依赖的作用域(effectScope)

// 2. 在 Pinia 的全局作用域中执行逻辑(隔离不同 Store 的上下文)
const result = pinia.scope.run(() => {
// 创建一个独立的 effect 作用域,用于管理当前 Store 的响应式副作用(如 watch/computed)
// 作用:Store 卸载时可通过 scope.stop() 清理所有依赖,避免内存泄漏
storeScope = effectScope();

// 3. 创建 Store 实例:
// - reactive:将 Store 转为响应式对象(符合 Vue 响应式机制)
// - createApis:Pinia 内部方法,给 Store 绑定核心 API(如 $state、$patch、$reset、$subscribe 等)
// - 传入 pinia 实例、Store id、storeScope:让 API 能访问全局状态和当前作用域
store = reactive(createApis(pinia, id, storeScope));

// 4. 在当前 Store 的独立作用域中,编译 setup 返回值(处理响应式状态)
return storeScope.run(() => compileSetup(pinia, id, setupStore));
});

// 5. 最终步骤:将 Store 实例挂载到 Pinia 上(全局可通过 pinia.state 或 useStore 访问)
// 返回挂载后的 Store 实例(供用户使用)
return setStore(pinia, store, id, result);
}

compileSetup

我们将纯响应式对象挂载到 pinia 实例中 储存到 state 里面

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
function compileSetup(pinia, id, setupStore) {
// 1. 初始化 Store 全局状态:如果 pinia.state 中没有当前 id 的状态,创建空对象
// pinia.state 是全局响应式状态容器,所有 Store 的状态都存在这里
!pinia.state.value[id] && (pinia.state.value[id] = {});

// 2. 遍历 setup 返回值的所有属性,区分「响应式状态」和「普通值」
for (let key in setupStore) {
const el = setupStore[key]; // 当前属性的值(可能是 ref/reactive/方法/普通值)

// 3. 核心判断:只将「纯 ref(非 computed)」或「reactive 对象」纳入 Pinia 全局状态
// - isRef(el) && !isComputed(el):computed 本质是 ref,但属于派生状态,不直接存到全局状态
// - isReactive(el):reactive 对象是引用类型响应式状态,需要纳入全局管理
if ((isRef(el) && !isComputed(el)) || isReactive(el)) {
// 将响应式状态挂载到 pinia.state.value[id] 下(全局可访问)
pinia.state.value[id][key] = el;
}
}

// 4. 返回完整的 setup 返回值(包含响应式状态、普通值、方法)
// 后续会合并到 Store 实例上,用户可通过 store.key 访问所有属性
return { ...setupStore };
}

这里的挂载方式是 state 的 value 中是一个键值对 我们给这个键值对赋值 从而储存好 state

createOptionsStore

1
2
3
4
5
6
7
8
9
10
11
12
function createOptionsStore(id, pinia, options) {
// options state getters actions
let store;
let storeScope;

const result = pinia.scope.run(() => {
storeScope = effectScope();
store = reactive(createApis(pinia, id, storeScope));
return storeScope.run(() => compileOptions(pinia, store, id, options));
});
return setStore(pinia, store, id, result, options.state);
}

基本逻辑是跟 setup 部分一致的 这里就不再写了 主要是在 compile 中有区别

compileOptions

1
2
3
4
5
6
7
8
9
10
11
function compileOptions(pinia, store, id, { state, getters, actions }) {
const storeState = createStoreState(pinia, id, state);
const storeGetters = createStoreGetters(store, getters);
const storeActions = createStoreActions(store, actions);

return {
...storeState,
...storeGetters,
...storeActions,
};
}

我们需要逐个去分析其中的 state getter action 目的是要将 option 里单独的部分最终转换为 setup 其中的形式 例如 getters 中的我们最终要换成 computed

createStoreState

1
2
3
4
function createStoreState(pinia, id, state) {
pinia.state.value[id] = state ? state() : {};
return toRefs(pinia.state.value[id]);
}

在 option 中 state 是返回的一个函数 所以如果当前 state 存在的话 我们直接执行这个 state 就能拿到值

当然 最后我们需要把它重新变为响应式 (参考 setup 中的写法)

createStoreGetters

1
2
3
4
5
6
function createStoreGetters(store, getters) {
return Object.keys(getters || {}).reduce((wrapper, getterName) => {
wrapper[getterName] = computed(() => getters[getterName].call(store));
return wrapper;
}, {});
}

这里的 reduce 是 JavaScript 数组的迭代累加方法,核心作用是:遍历数组的每一项,把它们 “合并” 成一个最终结果(对象 / 数字 / 数组等)

reduce 的两个关键参数

1
array.reduce(回调函数, 初始值)
  • 第一个参数:回调函数(每次遍历数组项时执行)

    回调函数里又有两个参数:

    • wrapper:「累加器」,每次循环都会把上一次的结果传给它(这里是 “正在构建的新 getters 对象”);
    • getterName:「当前项」,每次遍历到的数组元素(这里是 getters 对象的键名,比如 doubleCount)。
  • 第二个参数:初始值reduce 开始执行时,wrapper 的初始状态),这里是 {}(空对象)。

这段代码里的 reduce,本质是「遍历 getters 的所有键名,把每个 getter 函数包装成 computed 后,组装成一个新对象返回」。

createStoreActions

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
function createStoreActions(store, actions) {
const storeActions = {};
for (let actionName in actions) {
storeActions[actionName] = function () {
const afterList = [];
const errorList = [];
let res;

subscription.triggle(actionList, { after, onError });

try {
res = actions[actionName].apply(store, arguments);
} catch (err) {
subscription.triggle(errorList, err);
}

if (res instanceof Promise) {
return res
.then((r) => {
return subscription.triggle(afterList, r);
})
.catch((e) => {
subscription.triggle(errorList, e);
return Promise.reject(e);
});
}

subscription.triggle(afterList, res);
return res;

function after(cb) {
afterList.push(cb);
}
function onError(cb) {
errorList.push(cb);
}
};
}

return storeActions;
}

最基础的部分实际上就是

1
2
3
for (let actionName in actions) {
res = actions[actionName].apply(store, arguments);
}

即我们挨个执行一下 actionList 中的函数 其他部分是用来实现后面的 subscribe api 这里先按下不表

setStore

这是最终对 store 的整合

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function setStore(pinia, store, id, result, state) {
// 1. 把Store实例注册到Pinia全局容器中(通过id关联,后续可通过useXxxStore获取)
pinia.store.set(id, store);
// 2. 给Store挂载$id属性(标识当前Store的唯一ID,可通过store.$id访问)
store.$id = id;
// 3. 若有原始state,给Store添加$reset方法(用于重置state到初始状态,createReset是重置逻辑函数)
state && (store.$reset = createReset(store, state));
// 4. 把编译后的result(getters/actions等)合并到Store实例上(让store能直接访问store.getterName/store.actionName)
Object.assign(store, result);
// 5. 执行api 后面再谈
createState(pinia, id);
// 6. 执行Pinia的插件(触发插件的安装逻辑,比如日志、持久化等插件)
runPlugins(pinia, store);
// 7. 返回完全配置好的Store实例(此时Store已具备所有功能,可在组件/逻辑中使用)
return store;
}

$patch

store.$patch 是一个高效批量更新状态的 API,核心作用是:一次性修改多个 state 属性,或通过函数形式灵活更新状态,且仅触发一次响应式更新(性能更优)

执行中 我们可以直接返回一个键值对对象 也可以返回一个函数 用来修改状态

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
userStore.$patch({
address: {
...userStore.address, // 解构原有对象的所有属性
city: "上海", // 只修改需要的属性
},
});

userStore.$patch((state) => {
state.age += 1; // 直接操作 state 快照
state.address.area = "海淀区"; // 对象类型可直接修改深层属性(不会丢失其他属性)
// 支持复杂逻辑,比如循环
if (state.name === "李四") {
state.name = "王五";
}
});

所以 这里我们实现 patch 的主要逻辑是

1.当返回的是对象的时候 我们直接把这个新对象与原对象合并

2.当返回的是函数的时候 我们直接把它需要的 state 给传递进去然后执行函数即可

1
2
3
4
5
6
7
8
9
export const createPatch = (pinia, id) => {
return function $patch(stateorFn) {
if (typeof stateorFn === "function") {
stateorFn(pinia.state.value[id]);
} else {
mergeObject(stateorFn, pinia.state.value[id]);
}
};
};

然后我们在 utils 中封装一下合并对象的函数 这里不能直接用自带的 Object.assign 因为 state 中也可能含有 对象 我们需要进行深合并 防止某些属性丢失 所以我们需要逐个合并 并且进行递归

递归的思路是 如果新值和旧值都是对象的话,我们就继续合并 当一方不是会员的时候 我们直接赋值

1
2
3
4
5
6
7
8
9
10
11
12
export function mergeObject(newVals, oldVals) {
for (let key in newVals) {
const oldval = oldVals[key];
const newVal = newVals[key];

if (isObject(oldval) && isObject(newVal)) {
mergeObject(newVal, oldval);
} else {
oldVals[key] = newVal;
}
}
}

最后我们在 defineStore 中 封装一下 createApi 用来给 store 赋上这些 api

1
2
3
4
5
function createApis(pinia, id, scope) {
return {
$patch: createPatch(pinia, id),
};
}

我们只需要在创建 store 的时候调用一下这个方法即可(setup 同理)

在 createOptionsStore 与 createSetupStore 中

1
store = reactive(createApis(pinia, id, storeScope));

需要注意的是

**这里我们实现 api 的时候都是外层包裹函数用来接受参数 里面返回原 api 用的是闭包 **

$reset

核心作用是:将 Store 的 state 一键恢复到「初始定义时的状态」(比如 state 函数返回的原始值),无需手动逐个重置属性,简洁高效。

需要注意的是 这个方法只针对 options 写法 组合式的话我们只需要在里面自定义一个函数用来重置状态即可 比如

1
2
3
4
5
6
// 手动定义 $reset 逻辑(必须显式写)
reset: (store) => {
// store 是当前 Store 实例,直接重置 ref 值
store.count = 0;
store.items = [];
};

而在我们的 options 写法中 由于 state 本身就是一个函数 返回的是一个对象 并且这个对象就是元素原始值 故而我们只需要拿到最开始的对象 然后进行一次合并即可

1
2
3
4
5
6
7
8
export function createReset(store, stateFn) {
return function $reset() {
const initialState = stateFn ? stateFn() : {};
store.$patch((state) => {
Object.assign(state, initialState);
});
};
}

需要注意的是

$reset 的底层实现(createReset(store, state))需要两个核心参数,这两个参数只有在 setStore 执行时才同时就绪:

  1. store 实例:已经是被 Pinia 包装后的响应式对象(前面通过 reactive(createApis(...)) 创建),具备了基础的 Store 结构;
  2. state 函数:Store 定义时的原始 state 函数(用于生成初始状态快照,是 $reset 能 “重置” 的核心依据)。

所以我们直接在挂载 store 的时候引入这个方法

1
2
3
4
5
6
function setStore(pinia, store, id, result,state) {
...
state && (store.$reset = createReset(store,state))
...
}

$subscribe

subscribe监听 Store 状态变化 的核心方法,用于响应 Store 中状态的修改(包括直接修改、通过 action 修改、$patch 修改等),支持全局监听或局部监听,还能精准捕获变化细节(如变化的状态键、旧值、新值)。

在 pinia 的使用中

1
2
3
4
5
6
7
// 监听 Store 所有状态变化
const unsubscribe = userStore.subscribe((mutation, state) => {
// mutation:变化细节对象
// state:变化后的完整状态(等同于 userStore.$state)
console.log("状态变化详情:", mutation);
console.log("变化后状态:", state);
});

使用的时候会执行其中的回调函数

mutation 指向变化细节对象,那么其实就是

1
store.$subscribe(({ storeId }, state) => {});

所以在我们实现的时候我们需要传入一个回调函数,另外我们还需要传入 options 用来作为监听参数

1
function $subscribe(callback,options={})

在其中我们对变化的部分做监听即可

另外,我们还需要能使得 subscribe 能够受 scope 的管理 固然在外面再用 scope 包裹一下即可

1
2
3
4
5
6
7
8
9
10
11
12
13
export function createSubscribe(pinia, id, scope) {
return function $subscribe(callback, options = {}) {
scope.run(() =>
watch(
pinia.state.value[id],
(state) => {
callback({ storeId: id }, state);
},
options
)
);
};
}

我们拿到 id 监听对应部分 然后在 callback 中传入需要的两个部分即可

调用方式与 patch 相同

$onAction

在 Pinia 中,onAction 是一个核心订阅方法,用于监听 Store 中所有 Action 的执行过程(包括同步 / 异步 Action),支持在 Action 执行前、执行后、出错时触发回调,还能获取 Action 的名称、参数、返回值等关键信息。它是 Pinia 提供的 “副作用监听” 能力,常用于日志记录、埋点统计、数据校验、权限拦截等场景。

在实际使用中

1
2
3
4
5
6
7
8
const clg = store.$onAction(({ after, onError }) => {
onError((err) => {
console.log(err);
});
after(() => {
console.log("after", store.todoList);
});
});

核心需求是:

  1. 允许外部注册回调(cb),监听 Action 的执行过程;
  2. 回调能拿到 after(成功后触发)和 onError(失败后触发)两个方法,用于注册后续逻辑;
  3. 同步 Action 和异步 Action(返回 Promise)都要兼容;
  4. 遵循「订阅 - 触发」模式(先收集回调,再在对应时机执行)。

我们需要实现的大致逻辑是

保存回调函数 -> 发布订阅 -> 创建容器 -> 执行回调函数 -> 在 actions 中触发 -> actions 中创建列表逐个执行

首先我们封装一个工具函数用来收集回调函数 并且设定触发函数 用来控制回调函数的触发

1
2
3
4
5
6
7
8
export const subscription = {
add(list, cb) {
list.push(cb);
},
triggle(list, ...args) {
list.forEach((cb) => cb(...args));
},
};

在 api 中 声明一个全局的 actionList 用来存放 action

1
2
3
4
5
6
7
export let actionList = [];

export function createonAction() {
return function $onAction(cb) {
subscription.add(actionList, cb);
};
}

我们需要在执行解析 action 的环节添加新的逻辑用来处理 after 与 onerror 的逻辑

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
function createStoreActions(store, actions) {
const storeActions = {};
for (let actionName in actions) {
storeActions[actionName] = function () {
/*
每个 Action 执行时,都要单独维护一套 after/onError 回调(避免多个 Action 共用回调导致混乱);
这两个列表是「临时的」,只对当前这次 Action 执行有效(下次调用 Action 会重新初始化)。
*/
const afterList = []; //存储外部通过 after(cb) 注册的成功回调
const errorList = []; //存储外部通过 onError(cb) 注册的失败回调
let res;

subscription.triggle(actionList, { after, onError });

try {
res = actions[actionName].apply(store, arguments); // 执行原始 Action,绑定 store 为 this
} catch (err) {
subscription.triggle(errorList, err); // 同步 Action 出错时,触发所有 onError 回调
}

//处理异步 Action 的场景(Pinia 核心需求之一):如果原始 Action 返回 Promise,等待 Promise 状态变更后再触发对应回调;
if (res instanceof Promise) {
return res
.then((r) => {
return subscription.triggle(afterList, r); // 异步成功后,触发所有 after 回调
})
.catch((e) => {
subscription.triggle(errorList, e); // 异步失败后,触发所有 onError 回调
return Promise.reject(e); // 透传错误,让外部能捕获
});
}

/*
同步 Action 执行成功(没抛错),直接触发 afterList 中所有回调,同时返回原始结果;
确保同步和异步 Action 的 after 回调触发时机一致(都是在 Action 成功完成后)。
*/
subscription.triggle(afterList, res);
return res;

//储存回调
function after(cb) {
afterList.push(cb);
}
function onError(cb) {
errorList.push(cb);
}
};
}

return storeActions; // 每一个name对应的都是返回的res
}
1
subscription.triggle(actionList, { after, onError });
  • 这是「Action 执行前」的核心步骤:把 actionList 中所有外部注册的回调(通过 $onAction 传入的)全部触发;
  • 给每个回调传入 { after, onError } 两个方法 —— 这正是 Pinia onAction 回调的核心参数,让外部能通过这两个方法注册「成功后」和「失败后」的逻辑;
  • 这里的 after/onError 是当前 Action 专属的(步骤 3 定义的局部函数),外部调用 after(cb) 时,会把回调存入当前 Action 的 afterList,确保回调只对当前 Action 生效。

$dispose

在 Pinia 中,disposeStore 实例的核心生命周期方法,用于手动销毁 Store 并清理其关联资源(如订阅、监听器、副作用等),本质是触发 Store 的「卸载逻辑」,让 Store 从 Pinia 实例中移除并释放内存。

实现 dispose 我们需要实现下面几个功能

1.关闭当前的作用域 scope

2.将 actionList 重置为空

3.将 store 中的映射根据 id 删除掉

4.将 id 最底层储存的 state 也删除掉

1
2
3
4
5
6
7
8
export function createDispose(pinia, id, scope) {
return function $dispose() {
scope.stop();
actionList = [];
delete pinia.state.value[id];
pinia.store.delete(id);
};
}

需要注意的地方是 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
/**
* 为 Pinia 的 Store 实例创建并挂载 $state 属性
* 核心作用:实现 $state 对 Pinia 全局状态的代理访问 + 安全批量更新,对齐 Pinia 原生 $state 行为
* @param {Object} pinia - Pinia 全局实例(包含全局状态容器、Store 缓存等核心信息)
* @param {string} id - 当前 Store 的唯一标识(用于关联全局状态中的对应命名空间)
*/
export function createState(pinia, id) {
// 1. 从 Pinia 实例的 Store 缓存中,通过唯一 id 获取当前要处理的 Store 实例
// 保证 $state 属性挂载到正确的 Store 实例上(贴合 Pinia 单例 Store 设计)
const store = pinia.store.get(id);

// 2. 用 Object.defineProperty 给 Store 实例定义 $state 属性(存取描述符)
// 目的:精细控制 $state 的读取(get)和修改(set)行为,避免直接操作全局状态导致的问题
Object.defineProperty(store, "$state", {
/**
* 读取 $state 时的触发逻辑(store.$state 被访问时执行)
* 核心:$state 本身不存储数据,仅代理访问 Pinia 全局状态中当前 Store 对应的命名空间
* @returns {Object} 当前 Store 在全局状态中的响应式状态对象
*/
get: () => {
// key 为 Store 的 id,value 为对应 Store 的状态
return pinia.state.value[id];
},

/**
* 修改 $state 时的触发逻辑(store.$state = newState 时执行)
* 核心:不直接覆盖全局状态,而是通过 $patch 合并更新,避免误删原有状态属性
* @param {Object} newState - 用户传入的要更新的状态对象(部分属性或完整属性)
*/
set: (newState) => {
// 调用 Store 实例的 $patch 方法(Pinia 内置的状态更新方法)
// $patch 支持函数式更新,能批量处理状态变更,且会优化响应式触发(减少重复渲染)
store.$patch((state) => {
// Object.assign 实现浅合并:
// - 用 newState 的属性覆盖 state 中的同名属性
// - 保留 state 中 newState 未定义的原有属性(避免误删)
Object.assign(state, newState);
});
},
});
}

plugin 实现

在 Pinia 中,Plugin(插件) 是扩展 Store 功能的核心机制,允许你在不侵入 Store 定义的前提下,批量添加全局逻辑(如状态持久化、日志记录、权限校验等)。插件本质是一个函数,接收 context 上下文参数(包含 Pinia 实例、Store 实例等核心信息),可通过它修改 Store 行为、添加属性 / 方法、监听状态变化等。

Pinia 插件的工作流程:

  1. 插件是一个函数,接收 context 对象(包含 piniaappstore 等关键信息);
  2. 插件在 Store 初始化(第一次调用 useXXXStore())时执行;
  3. 可通过 context.store 访问 / 修改当前 Store 实例(如添加属性、监听状态、扩展方法);
  4. 支持返回一个对象,对象中的属性 / 方法会被合并到 Store 实例上(简化扩展逻辑);
  5. 多个插件按注册顺序执行,形成链式扩展。

Pinia 插件的核心流程是「注册插件 → 存储插件 → 执行插件

注册插件与储存插件

首先我们需要给 pinia 上挂载一个 plugins 数组

1
2
3
4
5
6
7
8
9
10
11
12
export default function createPinia() {
...
const plugins = []
...
function use(cb) {
plugins.push(cb)
return this
}
return {
plugins,
}
}

提供 use 方法(插件注册入口);

  1. plugins 数组(插件存储容器)保存所有注册的插件 (即 cb push 进数组);
  2. 返回包含 plugins 数组的 Pinia 实例(即 this),让后续逻辑能访问到已注册的插件。

执行插件

在 defineStore 中 我们封装一个执行插件的函数

1
2
3
4
5
6
7
8
function runPlugins(pinia, store) {
pinia.plugins.forEach((plugin) => {
const res = plugin({ store });
if (res) {
Object.assign(store, res);
}
});
}

核心作用是:在 Store 初始化时,批量执行所有已注册的插件,并将插件返回的扩展属性合并到 Store 实例上,最终实现「插件扩展 Store 功能」的核心目标。

传入 store 调用插件函数并且传入上下文对象(这里是简化过的 包含 store 的对象 在官方源码中可以传入更完整的 context 包括 pinia app options 等)

最后如果有 res 我们直接把返回的对象合并到 Store 实例上 这样就可以将插件返回的扩展属性合并到 Store 实例上

设计意图:通过「对象合并」实现插件对 Store 的「无痛扩展」,无需侵入 Store 内部定义,符合 Pinia 插件的「非侵入式」设计理念。

官方源码及解析

官方源码 :

[vuejs/pinia: 🍍 Intuitive, type safe, light and flexible Store for Vue using the composition api with DevTools support]:

源码解析:

[🍍 深入解析 Pinia 源码:一份全面的指南 Pinia 是 Vue 的存储库,它允许您跨组件/页面共享状态。 如果您 - 掘金]:

我们主要要从中 熟悉 pinia 的使用 ,熟练原生 js 的实现 ,理解 pinia 的设计原理 。