你没了解过的Pinia源码原理-Vue3

695 阅读4分钟

Pinia源码解析

Pinia🍍是目前Vue生态中最常用的状态管理库,允许在组件或页面之间共享状态。我对Pinia的源代码进行了一番探索,借此机会向大家介绍一下Pinia在Vue 3的setup中是如何发挥作用的。

CreatePinia

让我们来看看执行const pinia = createPinia()时,Pinia内部实际执行了哪些代码。

在阅读下面的内容之前,需要先了解effectScope、Vue插件以及全局注入的相关知识。如果您对这些概念还不太了解,可以通过以下链接进行了解:

image.png

首先,通过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 的内部实现。

image.png

在 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方式几乎没有使用过,这里我也不展开讲述。

image.png

createSetupStore中的代码有好几百行,但大部分的内容和相关DEV环境和DEVTOOL、VUE2,这里只关注Vue3 Setup 的代码部分。

image.png

image.png 可以看到partialStore提供几个内置函数,$onAction,$patch, $reset(vue3不支持,需在store中自定义),$subscribe,再使用reactive包装partialStore后赋值到store属性。

函数执行到此处我们传入defineStore中的setup函数还未执行,那么什么时候执行呢???
我们来看下面这段代码。

image.png

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实例化。

总结

  1. CreatePinia创建一个effectScope,全局state对象,Map对象
  2. defineStore判断setupStore还是OptionStore,返回一个useStore函数。
  3. useStore函数判断是否已存在store,如果未存在更具defineStore的判断来执行createSetupStore或者createOptionsStore,如果已存在直接返回已有store。
  4. createSetupStore 使用一个新的effectStore执行setup函数赋值给setupStore,最后将store和setupStore合并。