pinia 源码阅读

209 阅读1分钟

开始

不得不说,typescript 类型系统对于阅读源码的帮助太大了。能够快速理解函数的输入输出,从而理出整个大致的代码逻辑。

pinia 使用

根据pinia官方用例,pinia 的使用分为两步。

  1. 创建 store
  2. 使用 store

其中创建 store 使用 defineStore 函数。使用 store 使用 defineStore 函数返回的 hook 直接获取 store 实例。

defineStore 函数接收两种构建参数。

  • setup
  • options

setup 就是一个函数,返回具体的 ref 代理对象,options 选项则和 vuex 基本一致。但是也有差异,比如 pinia 没有专用同步选项 mutations,actions 同时支持同步和异步操作。具体差异可以自己查询。

官方示例

// stores/counter.js
import { defineStore } from 'pinia'

export const useCounterStore = defineStore('counter', {
  state: () => {
    return { count: 0 }
  },
  // could also be defined as
  // state: () => ({ count: 0 })
  actions: {
    increment() {
      this.count++
    },
  },
})
<script setup>
import { useCounterStore } from '@/stores/counter'

const counter = useCounterStore()

counter.count++
// with autocompletion ✨
counter.$patch({ count: counter.count + 1 })
// or using an action instead
counter.increment()
</script>

<template>
  <!-- Access the state directly from the store -->
  <div>Current Count: {{ counter.count }}</div>
</template>

defineStore 函数

defineStore 函数接收两种参数组合。

  1. id + options (或者直接在 options 中传入 id)
  2. id + setup + setupOptions

defineStore 函数的返回值是一个函数(useStore)。在使用时调用该函数获取 store 实例。

defineStore 函数做了两件事。

  1. 传入参数的初始化
  2. 生成 useStore 函数并返回。

参数初始化

传入参数的初始化其实是为了支持函数重载。做的事情非常简单,对 setup 和 options 做判断。这里定义了两个变量,id 和 options。用来缓存传入的 id 和 选项。如果传入的 setup 是函数,则 options 存储的是setupOptions ,否则为 setup

对应源码:

let id: string
  let options
  // 判断参数类型
  const isSetupStore = typeof setup === 'function'
  if (typeof idOrOptions === 'string') {
    id = idOrOptions
    options = isSetupStore ? setupOptions : setup
  } else {
    options = idOrOptions
    id = idOrOptions.id
  }

定义 useStore

useStore 接收两个可选参数,函数返回一个 store。 两个可选参数:

  • pinia 实例
  • hot 参数,主要用于热重载。pinia 内部处理时传递当前的 store 实例。

执行逻辑

暂时只考虑客户端渲染 Vue 项目的情况下的执行逻辑。

  1. 检查当前是否存在依赖注入上下文,即 Provide/Inject。
  2. 通过 inject 获取 pinia 实例。
  3. 判断 pinia 实例是否存在,存在则执行 setActivePinia,并将获取到的 pinia 实例传入。
  4. 将 activePinia 的值赋给当前 pinia 实例。
  5. 判断当前 id 的 store 在当前 pinia 中是否存在。
  6. 如果不存在则调用对应函数创建新 store(createSetupStore/createOptionsStore)。
  7. 如果存在直接获取并返回。

以上的执行逻辑存在几个问题。

  • 什么时候注入的 pinia 实例?
  • setActivePinia 做了什么?
  • 如何创建的 store?

注入 pinia 实例

当然可以在调用 useStore 函数的时候手动注入,但是一般在客户端渲染的情况下不需要手动注入(服务端渲染需要调用时创建 pinia 实例并手动注入,保证状态隔离),这是因为在 Vue 项目初始化时,会调用 createPinia 函数创建 pinia 实例并通过 app.use 方法注入。

createPinia

通过 Pinia 接口可以知道 pinia 的结构。

export interface Pinia {
  // 给 Vue 使用的安装函数
  install: (app: App) => void
  // 根 state
  state: Ref<Record<string, StateTree>>
  // 安装插件
  use(plugin: PiniaPlugin): Pinia
  // 安装的插件
  _p: PiniaPlugin[]
  // 当前连接 Vue app
  _a: App
  // 管理副作用的 Effect Scope。
  _e: EffectScope
  // store 仓库
  _s: Map<string, StoreGeneric>
  // 测试标识
  _testing?: boolean
}

首先是 install 函数,该函数接收一个参数,即 Vue app,也只做了一件事,执行setActivePinia 函数,参数为当前 pinia 对象。而 setActivePinia 函数会将传入的 pinia 缓存到activePinia 变量上,这样全局就只有一个 pinia。

而整个createPinia 函数做的事情为 初始化 pinia 对象并返回。再通过 app.use 函数注入。

这样就解答了上面两个问题,何时注入 pinia 和 setActivePinia 做了什么?

创建 store

由 defineStore 函数接收的参数其实可以知道,会有两个创建方法,分别对应传入 setup 函数和 options 选项的创建。

但是,最终都会使用 setup 创建,因为 setup 函数的返回值是具体的响应式数据,所以只需要将 options 传入的参数构造为一个 setup 函数再调用 createSetupStore 函数创建 store 即可。

setup 创建

传入 setup 函数的创建方法为 createSetupStore,参数有 4 个,返回一个 store。

参数:

  • id 唯一标识
  • setup 函数
  • options 配置项
  • pinia 对应 pinia
执行逻辑
  1. 在 pinia 上根据传入唯一 id 初始化pinia.state.value[$id] = {}
  2. 初始化 partialStore ,再根据 partialStore 使用 reactive 生成相应时数据 store。
  3. 将 store 挂载到 pinia._s 上 pinia._s.set($id, store as Store)
  4. 执行 setup 函数获取 setupStore。
  5. 遍历 setupStore 中的数据并处理。
  6. 合并 store 和 setupStore。
  7. store 对象的 $state 属性定义 getter 和 setter。
  8. 执行插件。
  9. 将 isListening isSyncListening 两个变量置为 true。
  10. return

结束

这是对 pinia 源代码的简单理解,也是一个阅读记录,如果有啥不对的,欢迎各位大佬指出。