- pinia 是由 vue 团队开发的,适用于 vue2 和 vue3 的状态管理库。
- 与 vue2 和 vue3 配套的状态管理库为 vuex3 和 vuex4,pinia被誉为 vuex5。
- pinia 没有命名空间模块;
- pinia 无需动态添加(底层通过 getCurrentInstance 获取当前 vue 实例进行关联);
- pinia 是平面结构(利于解构),没有嵌套,可以任意交叉组合。
学习本篇文章需要有pinia的使用有一定了解,推荐一篇pinia学习教程:大菠萝?Pinia已经来了,再不学你就out了
1.Pinia源码剖析
1.1.创建Pinia实例(createPinia)
在 main.js 文件中通过 createPinia
方法定义根 pinia 实例,以下就是createPinia函数源码
// packages/pinia/src/createPinia.ts 10行
export function createPinia(): Pinia {
const scope = effectScope(true)
const state = scope.run<Ref<Record<string, StateTree>>>(() =>
ref<Record<string, StateTree>>({})
)!
let _p: Pinia['_p'] = []
let toBeInstalled: PiniaPlugin[] = []
const pinia: Pinia = markRaw({
install(app: App) { // vue插件,所以实现了install方法
setActivePinia(pinia)
if (!isVue2) { //vue3
pinia._a = app // 在pinia中保存app实例
app.provide(piniaSymbol, pinia) // 在全局提供pinia,后续在defineStore中通过inject获取pinia
app.config.globalProperties.$pinia = pinia
if (USE_DEVTOOLS) { // 初始化pinia的插件(无需关心)
registerPiniaDevtools(app, pinia)
}
toBeInstalled.forEach((plugin) => _p.push(plugin))
toBeInstalled = []
}
},
use(plugin) {...},
_p, // pinia插件
_a: null, // app实例
_e: scope, // pinia实例的effect scope
_s: new Map<string, StoreGeneric>(), // 将子模块store,通过map统一管理(id与store映射)
state, // 全部store中的state
})
if (USE_DEVTOOLS && typeof Proxy !== 'undefined') {
pinia.use(devtoolsPlugin)
}
return pinia
}
1.2 定义Store(defineStore)
全局创建并注册 pinia 实例后,接下来我们可以定义需要全局管理状态的 store。定义 store 需要通过 defineStore
方法,defineStore
方法有四种传参方式
// packages/pinia/src/store.ts 802行
export function defineStore<...>( // 1
id: Id,
options: Omit<DefineStoreOptions<Id, S, G, A>, 'id'>
)
export function defineStore<...>( // 2
options: DefineStoreOptions<Id, S, G, A>): StoreDefinition<Id, S, G, A>
export function defineStore<...>( // 3
id: Id,
storeSetup: () => SS,
options?: DefineSetupStoreOptions<...>
)
export function defineStore( // 4
idOrOptions: any,
setup?: any,
setupOptions?: any
)
defineStore
方法首先根据传入参数,判断是 options 定义还是 setup 定义,然后定义内部函数 useStore
并返回
// packages/pinia/src/store.ts 854行
const isSetupStore = typeof setup === 'function' // 根据传入的参数判断是否是setupStore
if (typeof idOrOptions === 'string') {...
} else {...
}
function useStore(pinia?: Pinia | null, hot?: StoreGeneric): StoreGeneric {
const currentInstance = getCurrentInstance()
pinia =
(__TEST__ && activePinia && activePinia._testing ? null : pinia) ||
(currentInstance && inject(piniaSymbol, null)) // 获取pinia实例
if (pinia) setActivePinia(pinia)
if (__DEV__ && !activePinia) {...}
pinia = activePinia!
if (!pinia._s.has(id)) { // 对未注册的store进行注册
// creating the store registers it in `pinia._s`
if (isSetupStore) {
createSetupStore(id, setup, options, pinia) // 如果是函数就走createSetupStore
} else {
createOptionsStore(id, options as any, pinia) // 如果是对象就走createOptionStore
}
在 option 类型的定义方法 createOptionsStore
中,定义了一个 setup
方法,并将相关参数传入了 createSetupStore
方法创建一个 store 并返回,所以创建 store 的核心方式还是通过 createSetupStore
方法
// packages/pinia/src/store.ts 131行
function createOptionsStore(id, options, pinia, hot) {
const { state, actions, getters } = options;
const initialState = pinia.state.value[id];
let store;
function setup() {
if (!initialState && (!(process.env.NODE_ENV !== 'production') || !hot)) {...}
else {...}
}
const localState = (process.env.NODE_ENV !== 'production') && hot
?
toRefs(ref(state ? state() : {}).value)
: toRefs(pinia.state.value[id]); // 将state转成响应式的
// 将state、getters、actions合并到一个对象中返回。保持和setup一致,以便后续统一处理。
return assign(localState, actions, Object.keys(getters || {}).reduce((computedGetters, name) => {
if ((process.env.NODE_ENV !== 'production') && name in localState) {
console.warn(`[🍍]: A getter cannot have the same name as another state property. Rename one of them. Found with "${name}" in store "${id}".`);
}
computedGetters[name] = markRaw(computed(() => { // getters用computed进行处理
setActivePinia(pinia);
const store = pinia._s.get(id);
if (isVue2 && !store._r)
return;
return getters[name].call(store, store);
}));
return computedGetters;
}, {}));
}
store = createSetupStore(id, setup, options, pinia, hot, true); // 最后还是走createSetupStore
return store;
}
2.手写Pinia
2.1 createPina实现
import { markRaw } from "vue";
export const piniaSymbol = Symbol("pinia");
export function createPinia() {
const pinia = markRaw({
// 源码中也是用markRaw,将一个对象标记为不可被转为代理。返回该对象本身
install(app) {
pinia._a = app;
app.provide(piniaSymbol, pinia); // 将 pinia 实例注册到全局,便于所有子组件调用
app.config.globalProperties.$pinia = pinia;
},
_s: new Map(), // 存放所有子模块(单例模式)
});
return pinia;
}
2.2 defineStore实现
这边就不根据传入的参数类型不同去分别实现createOptionsStore和createSeupStore了,同学们感兴趣可以去实现。
import { reactive, computed, toRefs, inject, getCurrentInstance } from "vue";
import { piniaSymbol } from "./createPinia";
export function defineStore(id, options) {
const { state: stateFn, getters, actions } = options;
function useStore() {
// 获取组件实例
const currentInstance = getCurrentInstance();
// 获取pinia
const pinia = currentInstance && inject(piniaSymbol);
// 第一次使用时,将store set到pinia中
if (!pinia._s.has(id)) {
// 处理state(stateFn: () => {count: 0} -> state: {count: 0})
const state = reactive(stateFn());
// 处理getters
const getterComputed = Object.keys(getters || {}).reduce(
(getterComputed, getterName) => {
// 将getter通过computend进行包裹
getterComputed[getterName] = computed(() => {
console.log(store);
return getters[getterName].call(store, state); // 改变this指向store,下同
});
return getterComputed;
},
{}
);
// 处理actions
const actionFns = Object.keys(actions || {}).reduce(
(actionFns, actionName) => {
actionFns[actionName] = () => actions[actionName].call(store);
return actionFns;
},
{}
);
pinia._s.set(
id,
reactive({
...toRefs(state), // 确保state是响应式
...getterComputed,
...actionFns,
})
);
}
const store = pinia._s.get(id);
return store;
}
return useStore;
}
3.总结
实现pinia的核心思路如下
- 注册pinia插件时,通过provide将pinia实例注册到全局,在之后defineStore中通过inject获取pinia实例。
- 在pinia中获取_s(用于储存 id 和 store 实例的 map 映射,避免重复创建)。
- 将state通过reactive处理成响应式,将getters通过computed进行包裹,并且和actions通过call将this指向绑定为store。