pinia源码学习心得

pinia源码学习心得

首次读源码,无论体量大小,略感吃力。

ai盛行,能力也越来越强大,让ai为我们赋能是一件刻不容缓的事情。

首先让ai帮我生成了一份学习计划

prompt如下:

我是一个前端开发工程师,想要学习pinia项目源码,请分析项目结构,告诉我该如何进行学习

cursor生成的学习计划很详细,可用性也挺高。

pinia^1^

首先先谈一下,粗略阅读后,对pinia的理解吧

  • pinia允许创建多个pinia实例,因此会有activcePinia来保存当前激活的pinia实例。在 createPinia后,会将新创建的pinia实例进行激活。
  • 每个pinia都使用effectScope进行了作用域隔离,便于管理响应式数据,以及dispose后的数据清理
  • pinia下允许创建storedefinestore的所有store均在pinia._s存储了映射关系,并将 store 内的 state 数据合并到了 pinia 的state 上。
  • 每个 store 也进行了effectScope作用域隔离,用于单个store的响应式数据管理。
  • store 的创建支持options和setup两种形式,而createOptionsStore的作用其实是把 option 包装一层setup后,调用createSetupStore,因此createSetupStore是核心函数
  • pinia 层次清晰的进行了pinia实例、store的作用域隔离清晰,便于管理。
  • store 的数据更新,采用发布订阅机制来完成,维护了 subscriptions数组,存储$subscribe订阅的回调函数,$patch触发更新时,遍历并执行subscriptions的回调。其中$patch兼容了 {} 配置以及callback回调两种形式,对于配置式调用,将 store 的 state 进行合并,而callback回调的形式则是选择了将当前store的state传入后执行。
  • action的作用,执行我们 reurn 的函数,并遍历执行$onAction加入的回调
  • 我们可以在 $onAction添加回调,会被收集到actionSubscriptions,而在action 调用时会遍历执行,我们使用options编码时,action指向明确,自然会被收集,而setup式编写时,createOptionsStore是依据我们return出的函数进行调用 action,因此,我们如果想要监听definStore内我们一些异步函数,并进行后续同步操作的话,需要将该函数放入到return中。

下面记录一下,源码阅读历程

pinia架构

Pinia 是一个 monorepo 项目,采用 pnpm workspace 管理:

  • packages/pinia/ - 核心状态管理库 ⭐
  • packages/testing/ - 测试工具包
  • packages/nuxt/ - Nuxt.js 集成
  • packages/docs/ - 文档站点
  • packages/playground/ - 演示示例
  • scripts/ - 构建和发布脚本

pinia整体基于pnpm workspace实现monorepe架构策略,将pinia及其周边产品进行统一的仓库管理。

核心api实现

createpinia

创建pinia实例,

ts 复制代码
function createPinia(): Pinia {
  const scope = effectScope(true)
  // NOTE: here we could check the window object for a state and directly set it
  // if there is anything like it with Vue 3 SSR
  const state = scope.run<Ref<Record<string, StateTree>>>(() =>
    ref<Record<string, StateTree>>({})
  )!

  let _p: Pinia['_p'] = []
  // plugins added before calling app.use(pinia)
  let toBeInstalled: PiniaPlugin[] = []

  const pinia: Pinia = markRaw({
    install(app: App) {
      // this allows calling useStore() outside of a component setup after
      // installing pinia's plugin
      setActivePinia(pinia)
      pinia._a = app
      app.provide(piniaSymbol, pinia)
      app.config.globalProperties.$pinia = pinia
      /* istanbul ignore else */
      if (__USE_DEVTOOLS__ && IS_CLIENT) {
        registerPiniaDevtools(app, pinia)
      }
      toBeInstalled.forEach((plugin) => _p.push(plugin))
      toBeInstalled = []
    },

    use(plugin) {
      if (!this._a) {
        toBeInstalled.push(plugin)
      } else {
        _p.push(plugin)
      }
      return this
    },

    _p,
    // it's actually undefined here
    // @ts-expect-error
    _a: null,
    _e: scope,
    _s: new Map<string, StoreGeneric>(),
    state,
  })

  // pinia devtools rely on dev only features so they cannot be forced unless
  // the dev build of Vue is used. Avoid old browsers like IE11.
  if (__USE_DEVTOOLS__ && IS_CLIENT && typeof Proxy !== 'undefined') {
    pinia.use(devtoolsPlugin)
  }

  return pinia
}
effectScope

使用vue的effectScope​api进行顶层作用域的隔离,方便在dispose​之后,对响应式数据的清理

install

pinia是作为插件/工具库的形式出现的,为了贴合vue的插件集成机制,createPinia方法暴露出install,用于vue对pinia实例的全局挂载。

ts 复制代码
install(app: App) {
  // this allows calling useStore() outside of a component setup after
  // installing pinia's plugin
  setActivePinia(pinia)
  pinia._a = app
  app.provide(piniaSymbol, pinia)
  app.config.globalProperties.$pinia = pinia
  /* istanbul ignore else */
  if (__USE_DEVTOOLS__ && IS_CLIENT) {
    registerPiniaDevtools(app, pinia)
  }
  toBeInstalled.forEach((plugin) => _p.push(plugin))
  toBeInstalled = []
}
use

pinia提供了一套插件系统集成的机制,用于扩展pinia的功能,eg:持久化工具pinia-plugin-persistedstat

用户所添加的插件可能发生在,vue挂载pinia之前或之后完成,因此进行兼容性处理,

ts 复制代码
use(plugin) {
  if (!this._a) {
    toBeInstalled.push(plugin)
  } else {
    _p.push(plugin)
  }
  return this
}

defineStore

使用TS重载约定了两种使用方式

ts 复制代码
export function defineStore<
  Id extends string,
  S extends StateTree = {},
  G extends _GettersTree<S> = {},
  // cannot extends ActionsTree because we loose the typings
  A /* extends ActionsTree */ = {},
>(
  id: Id,
  options: Omit<DefineStoreOptions<Id, S, G, A>, 'id'>
): StoreDefinition<Id, S, G, A>

/**
 * Creates a `useStore` function that retrieves the store instance
 *
 * @param id - id of the store (must be unique)
 * @param storeSetup - function that defines the store
 * @param options - extra options
 */
export function defineStore<Id extends string, SS>(
  id: Id,
  storeSetup: (helpers: SetupStoreHelpers) => SS,
  options?: DefineSetupStoreOptions<
    Id,
    _ExtractStateFromSetupStore<SS>,
    _ExtractGettersFromSetupStore<SS>,
    _ExtractActionsFromSetupStore<SS>
  >
): StoreDefinition<
  Id,
  _ExtractStateFromSetupStore<SS>,
  _ExtractGettersFromSetupStore<SS>,
  _ExtractActionsFromSetupStore<SS>
>

做了参数兼容,判断第二位参数传入了setup,则使用第三个传参作为optipn

ts 复制代码
function defineStore(
  // TODO: add proper types from above
  id: any,
  setup?: any,
  setupOptions?: any
): StoreDefinition {
  let options:
    | DefineStoreOptions<
        string,
        StateTree,
        _GettersTree<StateTree>,
        _ActionsTree
      >
    | DefineSetupStoreOptions<
        string,
        StateTree,
        _GettersTree<StateTree>,
        _ActionsTree
      >

  const isSetupStore = typeof setup === 'function'
  // the option store setup will contain the actual options in this case
  options = isSetupStore ? setupOptions : setup
  function useStore(pinia?: Pinia | null, hot?: StoreGeneric): StoreGeneric {
    	...
  }
}
useStore

先确保当前的pinia的激活

ts 复制代码
if (pinia) setActivePinia(pinia)

判断当前 id 是否已经创建了store

ts 复制代码
if (!pinia._s.has(id)) {
  // creating the store registers it in `pinia._s`
  if (isSetupStore) {
	// 函数式调用的store
    createSetupStore(id, setup, options, pinia)
  } else {
	// 对象式调用的store
    createOptionsStore(id, options as any, pinia)
  }

  /* istanbul ignore else */
  if (__DEV__) {
    // @ts-expect-error: not the right inferred type
    useStore._pinia = pinia
  }
}

开发环境下的热更新机制

_hotUpdate ​的实现后续补充

ts 复制代码
if (__DEV__ && hot) {
  const hotId = '__hot:' + id
  const newStore = isSetupStore
    ? createSetupStore(hotId, setup, options, pinia, true)
    : createOptionsStore(hotId, assign({}, options) as any, pinia, true)

  hot._hotUpdate(newStore)

  // cleanup the state properties and the store from the cache
  delete pinia.state.value[hotId]
  pinia._s.delete(hotId)
}
createOptionsStore
ts 复制代码
function createOptionsStore<
  Id extends string,
  S extends StateTree,
  G extends _GettersTree<S>,
  A extends _ActionsTree,
>(
  id: Id,
  options: DefineStoreOptions<Id, S, G, A>,
  pinia: Pinia,
  hot?: boolean
): Store<Id, S, G, A> {
  const { state, actions, getters } = options

  const initialState: StateTree | undefined = pinia.state.value[id]

  let store: Store<Id, S, G, A>

  function setup() {
    if (!initialState && (!__DEV__ || !hot)) {
      /* istanbul ignore if */
      pinia.state.value[id] = state ? state() : {}
    }

    // avoid creating a state in pinia.state.value
    const localState =
      __DEV__ && hot
        ? // use ref() to unwrap refs inside state TODO: check if this is still necessary
          toRefs(ref(state ? state() : {}).value)
        : toRefs(pinia.state.value[id])

    return assign(
      localState,
      actions,
      Object.keys(getters || {}).reduce(
        (computedGetters, name) => {
          if (__DEV__ && 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(() => {
              setActivePinia(pinia)
              // it was created just before
              const store = pinia._s.get(id)!

              // allow cross using stores

              // @ts-expect-error
              // return getters![name].call(context, context)
              // TODO: avoid reading the getter while assigning with a global variable
              return getters![name].call(store, store)
            })
          )
          return computedGetters
        },
        {} as Record<string, ComputedRef>
      )
    )
  }

  store = createSetupStore(id, setup, options, pinia, hot, true)

  return store as any
}

createOptionsStore​的只是将options​的选项解耦处理后,包装成了包含 return 的setup方法,然后调用createSetupStore

createSetupStore

createPinia ​一样,使用effectScope​进行store的作用域隔离。

createSetupStore​的作用就是创建store​,将store​挂载到pinia._s​上,最后将其 return

ts 复制代码
function createSetupStore<
  Id extends string,
  SS extends Record<any, unknown>,
  S extends StateTree,
  G extends Record<string, _Method>,
  A extends _ActionsTree,
>(
  $id: Id,
  setup: (helpers: SetupStoreHelpers) => SS,
  options:
    | DefineSetupStoreOptions<Id, S, G, A>
    | DefineStoreOptions<Id, S, G, A> = {},
  pinia: Pinia,
  hot?: boolean,
  isOptionsStore?: boolean
): Store<Id, S, G, A> {
  ...

  const partialStore = {
    _p: pinia,
    // _s: scope,
    $id,
    $onAction: addSubscription.bind(null, actionSubscriptions),
    $patch,
    $reset,
    $subscribe(callback, options = {}) {
      const removeSubscription = addSubscription(
        subscriptions,
        callback,
        options.detached,
        () => stopWatcher()
      )
      const stopWatcher = scope.run(() =>
        watch(
          () => pinia.state.value[$id] as UnwrapRef<S>,
          (state) => {
            if (options.flush === 'sync' ? isSyncListening : isListening) {
              callback(
                {
                  storeId: $id,
                  type: MutationType.direct,
                  events: debuggerEvents as DebuggerEvent,
                },
                state
              )
            }
          },
          assign({}, $subscribeOptions, options)
        )
      )!

      return removeSubscription
    },
    $dispose,
  } as _StoreWithState<Id, S, G, A>
  const store: Store<Id, S, G, A> = reactive(
    __DEV__ || (__USE_DEVTOOLS__ && IS_CLIENT)
      ? assign(
          {
            _hmrPayload,
            _customProperties: markRaw(new Set<string>()), // devtools custom properties
          },
          partialStore
          // must be added later
          // setupStore
        )
      : partialStore
  ) as unknown as Store<Id, S, G, A>
  // 将 store 挂载
  pinia._s.set($id, store as Store)
  ...
  return store
}
$subscribe

subscriptions​添加回调函数,用于$patch​执行时触发。

ts 复制代码
function $subscribe(callback, options = {}) {
  const removeSubscription = addSubscription(
    subscriptions,
    callback,
    options.detached,
    () => stopWatcher()
  )
  const stopWatcher = scope.run(() =>
    watch(
      () => pinia.state.value[$id] as UnwrapRef<S>,
      (state) => {
        if (options.flush === 'sync' ? isSyncListening : isListening) {
          callback(
            {
              storeId: $id,
              type: MutationType.direct,
              events: debuggerEvents as DebuggerEvent,
            },
            state
          )
        }
      },
      assign({}, $subscribeOptions, options)
    )
  )!

  return removeSubscription
}
addSubscription​的作用就是向subscriptions​pushcallback​,并返回一个移除该callback​的回调,当detached​为 true 时,内部也使用了onScopeDispose​监听EffectScope​(作用域)被销毁时自动执行清理操作
ts 复制代码
function addSubscription<T extends _Method>(
  subscriptions: T[],
  callback: T,
  detached?: boolean,
  onCleanup: () => void = noop
) {
  subscriptions.push(callback)

  const removeSubscription = () => {
    const idx = subscriptions.indexOf(callback)
    if (idx > -1) {
      subscriptions.splice(idx, 1)
      onCleanup()
    }
  }

  if (!detached && getCurrentScope()) {
    onScopeDispose(removeSubscription)
  }

  return removeSubscription
}

$patch

支持传入函数、对象

ts 复制代码
  function $patch(stateMutation: (state: UnwrapRef<S>) => void): void
  function $patch(partialState: _DeepPartial<UnwrapRef<S>>): void

如果传入回调,则执行回调;

如果传入对象,则进行state合并;

然后遍历触发subscriptions​,而subscriptions​是$subscribe​添加的

ts 复制代码
function $patch(
    partialStateOrMutator:
      | _DeepPartial<UnwrapRef<S>>
      | ((state: UnwrapRef<S>) => void)
  ): void {
    let subscriptionMutation: SubscriptionCallbackMutation<S>
    isListening = isSyncListening = false
    // reset the debugger events since patches are sync
    /* istanbul ignore else */
    if (__DEV__) {
      debuggerEvents = []
    }
    if (typeof partialStateOrMutator === 'function') {
	// 如果传入回调,则执行回调;
      partialStateOrMutator(pinia.state.value[$id] as UnwrapRef<S>)
      subscriptionMutation = {
        type: MutationType.patchFunction,
        storeId: $id,
        events: debuggerEvents as DebuggerEvent[],
      }
    } else {
	  // 如果传入对象,则进行state合并
      mergeReactiveObjects(pinia.state.value[$id], partialStateOrMutator)
      subscriptionMutation = {
        type: MutationType.patchObject,
        payload: partialStateOrMutator,
        storeId: $id,
        events: debuggerEvents as DebuggerEvent[],
      }
    }
    const myListenerId = (activeListener = Symbol())
    nextTick().then(() => {
      if (activeListener === myListenerId) {
        isListening = true
      }
    })
    isSyncListening = true
    // because we paused the watcher, we need to manually call the subscriptions
    // 执行 $subscribe 添加的的回调
	triggerSubscriptions(
      subscriptions,
      subscriptionMutation,
      pinia.state.value[$id] as UnwrapRef<S>
    )
  }

mergeReactiveObjects

这里贴出代码的目的是,由于 mergeReactiveObjects​ 是通过遍历赋值实现更新,而不是全覆盖,所以明确当我们使用对象式的$patch​时,如果需要属性置空,也是要进行一个空值的属性赋值

ts 复制代码
function mergeReactiveObjects<
  T extends Record<any, unknown> | Map<unknown, unknown> | Set<unknown>,
>(target: T, patchToApply: _DeepPartial<T>): T {
  // Handle Map instances
  if (target instanceof Map && patchToApply instanceof Map) {
    patchToApply.forEach((value, key) => target.set(key, value))
  } else if (target instanceof Set && patchToApply instanceof Set) {
    // Handle Set instances
    patchToApply.forEach(target.add, target)
  }

  // no need to go through symbols because they cannot be serialized anyway
  for (const key in patchToApply) {
    if (!patchToApply.hasOwnProperty(key)) continue
    const subPatch = patchToApply[key]
    const targetValue = target[key]
    if (
      isPlainObject(targetValue) &&
      isPlainObject(subPatch) &&
      target.hasOwnProperty(key) &&
      !isRef(subPatch) &&
      !isReactive(subPatch)
    ) {
      // NOTE: here I wanted to warn about inconsistent types but it's not possible because in setup stores one might
      // start the value of a property as a certain type e.g. a Map, and then for some reason, during SSR, change that
      // to `undefined`. When trying to hydrate, we want to override the Map with `undefined`.
      target[key] = mergeReactiveObjects(targetValue, subPatch)
    } else {
      // @ts-expect-error: subPatch is a valid value
      target[key] = subPatch
    }
  }

  return target
}

Footnotes

pinia

我来帮你分析 Pinia 项目的结构,并制定一个学习源码的路径。让我先探索项目的整体结构。

🍍 Pinia 项目源码学习指南

根据我对 Pinia 项目结构的分析,这是一个非常好的状态管理库源码学习项目。以下是我为你制定的学习路径:

📁 项目结构概览

Pinia 是一个 monorepo 项目,采用 pnpm workspace 管理:

  • packages/pinia/ - 核心状态管理库 ⭐
  • packages/testing/ - 测试工具包
  • packages/nuxt/ - Nuxt.js 集成
  • packages/docs/ - 文档站点
  • packages/playground/ - 演示示例
  • scripts/ - 构建和发布脚本

📚 推荐学习路径

🎯 第一阶段:理解核心概念 (1-2天)
  1. README.md 开始

    • 了解 Pinia 的设计理念和特点
    • 理解为什么它是 Vuex 的继承者
  2. 查看基本使用示例

    12:15:packages/playground 复制代码
    - 先运行 playground 示例
    - 理解 defineStore、createPinia 的基本用法
🔧 第二阶段:核心源码剖析 (3-5天)

按以下顺序阅读核心文件:

  1. 入口文件 - packages/pinia/src/index.ts

    • 了解整体 API 导出结构
    • 掌握核心概念的命名
  2. Pinia 实例创建 - packages/pinia/src/createPinia.ts

    typescript 复制代码
    - createPinia() 函数实现
    - effectScope 的使用
    - 插件系统设计
  3. 根状态管理 - packages/pinia/src/rootStore.ts

    typescript 复制代码
    - 全局状态管理机制
    - setActivePinia/getActivePinia
    - 插件接口定义
  4. 核心存储实现 - packages/pinia/src/store.ts​ ⭐⭐⭐

    typescript 复制代码
    - defineStore 函数 (行 837-948)
    - createOptionsStore vs createSetupStore
    - Store 的生命周期管理
  5. 类型定义 - packages/pinia/src/types.ts

    typescript 复制代码
    - TypeScript 类型系统设计
    - Store、State、Getters、Actions 类型定义
🧪 第三阶段:测试和实践 (2-3天)
  1. 阅读测试用例 - packages/pinia/__tests__/

    12:15:packages/pinia/__tests__/store.spec.ts 复制代码
    - 理解 API 的预期行为
    - 学习最佳实践
  2. 辅助功能

    • storeToRefs.ts - 响应式引用转换
    • mapHelpers.ts - Vue 2 兼容的辅助函数
    • subscriptions.ts - 订阅机制
🛠 第四阶段:高级特性 (2-3天)
  1. 开发工具集成 - packages/pinia/src/devtools/
  2. 热重载支持 - packages/pinia/src/hmr.ts
  3. SSR 支持 - 查看 SSR 相关测试
  4. 插件生态 - packages/testing/packages/nuxt/

🎯 学习建议

📖 阅读顺序
  1. 广度优先: 先理解整体架构
  2. 深度优先 : 重点研究 store.ts 文件
  3. 实践导向: 结合测试用例理解 API
🔍 关键技术点

Vue 3 特性应用:

  • Composition API
  • effectScope
  • 响应式系统 (reactive, ref, computed)
  • 依赖注入 (provide/inject)

TypeScript 高级特性:

  • 泛型约束
  • 条件类型
  • 工具类型
  • 类型推断

设计模式:

  • 单例模式 (Store 实例管理)
  • 插件模式 (PiniaPlugin)
  • 观察者模式 (订阅机制)
🛠 实践练习
  1. 克隆并运行项目

    bash 复制代码
    git clone https://github.com/vuejs/pinia.git
    cd pinia
    pnpm install
    pnpm dev  # 运行测试
  2. 调试核心函数

    • defineStore 中加 console.log
    • 观察 Store 创建过程
  3. 编写简单插件

    • 实现一个状态持久化插件
    • 理解插件机制

💡 学习成果验证

完成学习后,你应该能够:

  1. 📝 解释 Pinia 的核心架构设计
  2. 🔧 实现 自定义 Store 和插件
  3. 🎯 优化 Vue 应用的状态管理
  4. 🐛 调试 Pinia 相关问题
  5. 📚 贡献 开源代码

📚 额外资源

这个学习路径大约需要 1-2 周时间,可以根据你的 经验水平调整节奏。重点是理解 Vue 3 的响应式系统和 TypeScript 的类型系统在状态管理中的应用!