Pinia源码解析
Pinia🍍是目前Vue生态中最常用的状态管理库,允许在组件或页面之间共享状态。我对Pinia的源代码进行了一番探索,借此机会向大家介绍一下Pinia在Vue 3的setup中是如何发挥作用的。
CreatePinia
让我们来看看执行const pinia = createPinia()时,Pinia内部实际执行了哪些代码。
在阅读下面的内容之前,需要先了解effectScope、Vue插件以及全局注入的相关知识。如果您对这些概念还不太了解,可以通过以下链接进行了解:
首先,通过effectScope(true)创建了一个独立的effect作用域,并使用scope.run创建了一个Ref类型的状态对象。
我们重点关注一下Pinia的install方法。在install方法中,将当前的Pinia设置为活跃状态(之后可以通过getActivePinia获取当前的Pinia),并将Pinia注册到Vue的provide/inject系统中,并设置全局属性。
另外,解释一下Pinia中的其他几个属性,以便后续的理解:
_p; // 指向的是 plugin
_a; // 指向的是 app
_e; // 指向 effect 作用域
_s; // 其实是通过 Map 的方式,通过 id 来 set、get 指定的 store。id 即是我们在 defineStore 中传入的第一个参数。后续文章会详细解释 store。
至于use方法,则是用于提供给Pinia的插件,如persist插件。本文不展开讨论插件相关内容。最后,createPinia返回了此函数中创建的Pinia,即我们在main.ts中使用app.use(pinia)注册的Pinia。
const pinia = createPinia()
app.use(pinia)
defineStore
通常,我们会在 src/stores 中使用 defineStore 来定义应用程序中所需的存储,并在其他地方使用 useXxxStore 来创建并使用它。接下来,我们来讲述 defineStore 的内部实现。
在 Pinia 中,我们可以通过两种方式来使用 options 选项或 setup 函数来创建 useStore。以上代码的作用实际上是通过检查我们在定义时传入的 setup 参数类型是否为函数来确定是否为 setupStore,并获取我们传入 store 的 id。
后面pinia编写了一个useStore函数并设置了$id属性,并在defineStore中返回了此函数,其实就是我们定义的useXxxStore,以下代码是源码的缩略(忽略了DEV、TEST环境的执行代码)
function useStore(pinia?: Pinia | null, hot?: StoreGeneric): StoreGeneric {
const hasContext = hasInjectionContext()
// 官方介绍此API为 如果 inject() 可以在错误的地方 (例如 setup() 之外) 被调用而不触发警告,则返回 true。此方法适用于希望在内部使用 inject() 而不向用户发出警告的库。
pinia = hasContext ? inject(piniaSymbol, null) : null
// 上文讲过通过app.provide(piniaSymbol,pinia)注入
// _s 是上文我们讲述过的map,结构为 Map<id,store> ,不存在就通过create创建,否则直接通过get来获取store
// 此处也是解释了为什么我们在代码中多处使用useStore而不会多次创建相同的store(单例模式)。
// 还有一个点就是当我们使用defineStore定义了一个store而在代码中未使用useStore调用的话,
// 它在应用中是不会被挂载的,我由此推断应该也是不会被进行打包的。
if (!pinia._s.has(id)) {
// creating the store registers it in `pinia._s`
if (isSetupStore) {
createSetupStore(id, setup, options, pinia)
} else {
createOptionsStore(id, options as any, pinia)
}
const store: StoreGeneric = pinia._s.get(id)!
return store as any
}
createSetupStore
createOptionsStore和createSetupStore是上文在useStore的代码片段中出现的两个create函数,为何我在这里只讲述createSetupStore,由下文源码可以看出createOptionStore内部最终是通过用户传入options中的state,actions,getters去创建一个setup函数最后再去调用createSetupStore,由于我options方式几乎没有使用过,这里我也不展开讲述。
createSetupStore中的代码有好几百行,但大部分的内容和相关DEV环境和DEVTOOL、VUE2,这里只关注Vue3 Setup 的代码部分。
可以看到partialStore提供几个内置函数,$onAction,$patch, $reset(vue3不支持,需在store中自定义),$subscribe,再使用reactive包装partialStore后赋值到store属性。
函数执行到此处我们传入defineStore中的setup函数还未执行,那么什么时候执行呢???
我们来看下面这段代码。
runWithContext的作用就是传入回调函数并且立即执行,且回调函数可以在即使没有活动的组件实例的情况下使用inject函数。
所以我们可以通过pinia的_e即在createPinia时创建的那个effectscope去执行创建一个嵌套的effectScope执行我们在defineStore时传入的setup函数,从而去定义ref,computed等属性,最后再使用Object.assign函数将store和setupStore合并。
最后解释一下pinia是如何巧妙解决store中互相引用导致的无限循环问题,也是pinia为什么要先创建partialStore,最后再利用effctScope执行setup函数的原因。
以下面这段代码举例:
// UserStore.ts
import { defineStore } from 'pinia';
import { useProductStore } from './ProductStore';
export const useUserStore = defineStore('user', () => {
const productStore = useProductStore();
// ...other code
});
// ProductStore.ts
import { defineStore } from 'pinia';
import { useUserStore } from './UserStore';
export const useProductStore = defineStore('product', () => {
const userStore = useUserStore();
// ...other code
});
举例useUserStore和useProductStore,setup函数中都互相使用对方的useStore函数。
假设我们在代码中使用useUserStore,通过上文介绍的原理解析中可知Pinia是先创建userStore的部分实例并挂载到pinia中,然后再使用effectScope来执行传入的setup代码块,那么执行userStore的setup代码块中的useProductStore()时,其中使用useUserStore会直接返回userStore的部分实例,因为pinia已经存储了useUserStore的部分实例,而不会再次进入userStore的create实例化。
总结
- CreatePinia创建一个effectScope,全局state对象,Map对象
- defineStore判断setupStore还是OptionStore,返回一个useStore函数。
- useStore函数判断是否已存在store,如果未存在更具defineStore的判断来执行createSetupStore或者createOptionsStore,如果已存在直接返回已有store。
- createSetupStore 使用一个新的effectStore执行setup函数赋值给setupStore,最后将store和setupStore合并。