浅读 Pinia 源码

283 阅读6分钟

rootStore.ts

Pinia 实例

  • install: (app: App) => void :

    • 当你调用 app.use(pinia) 时,实际上就是调用这个 install 函数,它会执行一些必要的初始化操作,例如设置全局 Pinia 实例、将 Pinia 注入到依赖注入上下文等。
  • state: Ref<Record<string, StateTree>>  :

    • 存储 Pinia 实例中的所有 store 的根状态。它是一个响应式对象,key 是 store 的 id(字符串),value 是 store 的状态(StateTree,通常是一个普通 JavaScript 对象)。
  • use(plugin: PiniaPlugin): Pinia :

    • Pinia 插件可以扩展 store 的功能,例如添加持久化、devtools 集成等。use 方法提供了一种灵活的方式来扩展 Pinia 的功能。
  • _p: PiniaPlugin[]  :

    • 作用:  存储所有已注册的 Pinia 插件。
    • 意义:  用于跟踪所有已安装的插件,在创建 store 的时候会被调用,以便应用这些插件。
  • _a: App :

    • 作用:  存储与 Pinia 实例关联的 Vue 应用实例。
    • 意义:  Pinia 需要访问 Vue 应用实例来执行一些操作,例如注册 devtools 插件。
  • _e: EffectScope :

    • 作用:  Pinia 实例的 effect scope。Effect Scope可以用来控制一组副作用 effect (例如 computed、watch) 的激活时机。
    • 意义:  用于管理 Pinia 实例的副作用。
  • _s: Map<string, StoreGeneric>  :

    • 存储所有已创建的 store。Key 是 store 的 id(字符串),value 是 store 实例(StoreGeneric,一个通用的 store 类型)。
    • StoreGeneric
    export type StoreGeneric = Store<
      string,
      StateTree,
      _GettersTree<StateTree>,
      _ActionsTree
    >
    
  • _testing?: boolean :

    • 用于在测试环境中绕过 useStore(pinia)

activePinia

全局变量,记录当前pinia实例

export let activePinia: Pinia | undefined

PiniaPluginContext And PiniaPlugin 不做研究

export interface PiniaPlugin {
  /**
   * Plugin to extend every store. Returns an object to extend the store or
   * nothing.
   *
   * @param context - Context
   */
  (
    context: PiniaPluginContext
  ): Partial<PiniaCustomProperties & PiniaCustomStateProperties> | void
}

createPinia.ts

createPinia(): Pinia

  • 声明一个EffectScope,使用run方法传入一个efect,返回一个state
  • 创建一个Pinia实例,使用markRaw()去除响应式,最后return 这个pinia 实例
export function createPinia(): Pinia {
    const scope = effectScope(true)
    const state = scope.run<Ref<Record<string, StateTree>>>(() =>
        ref<Record<string, StateTree>>({})
    )!
    // ... 省略插件执行
    const pinia: = markRaw({
        install(app: App) {
          // 作用: 设置当前激活的 Pinia 实例。
          // 意义:全局变量,记录当前pinia实例
          // 代码:export const setActivePinia: _SetActivePinia = (pinia) => (activePinia = pinia)
          setActivePinia(pinia)
          // 将 Vue 应用实例 (app) 赋值给 Pinia 实例的 _a 属性。
          // Pinia 需要访问 Vue 应用实例,来注册devtools
          pinia._a = app
          // 作用: 使用 Vue 的 provide 功能,将 Pinia 实例注入到 Vue 应用的依赖注入上下文中。
          // 意义: 让所有的组件都可以用 `inject(piniaSymbol)`来获取pinia实例
          app.provide(piniaSymbol, pinia)
          // 作用: 将 Pinia 实例添加到 Vue 应用的全局属性中,通过 $pinia 访问 Pinia 实例
          app.config.globalProperties.$pinia = pinia
      },
      // ... 省略use 就是添加插件
       
    }) 
}

disposePinia(pinia: Pinia)

销毁一个pinia实例,并且删除他的EffectScope state,plugins和store,销毁后不能再次使用

export function disposePinia(pinia: Pinia) {
  pinia._e.stop()
  pinia._s.clear()
  pinia._p.splice(0)
  pinia.state.value = {}
  // @ts-expect-error: non valid
  pinia._a = null
}

store.ts

createSetupStore() 重点!!!

负责创建和初始化 Pinia store 实例

  • 创建响应式state
  • 处理 actions
  • 应用插件
  • 设置HMR
  • 与devtools集成

函数签名和泛型

function createSetupStore<
  Id extends string, // storeId
  SS extends Record<any, unknown>, // setup 函数
  S extends StateTree,  // state
  G extends Record<string, _Method>, // getter
  A extends _ActionsTree, // action
>(
  $id: Id,
  setup: (helpers: SetupStoreHelpers) => SS,
  options:
    | DefineSetupStoreOptions<Id, S, G, A>
    | DefineStoreOptions<Id, S, G, A> = {},// 配置 store 的选项。可以包含 state 的初始值、getters、actions 等
  pinia: Pinia,
  hot?: boolean,// 是否启用热模块替换(hmr)
  isOptionsStore?: boolean //是否是 Options API 形式的store
)

内部变量和初始化

// 一个 effect scope,用于管理 store 的副作用
let scope!: EffectScope
// 合并了 actions 的选项对象,用于插件。
const optionsForPlugin: DefineStoreOptionsInPlugin<Id, S, G, A> = assign(
    { actions: {} as A },
    options
)
/* 开发环境下的检查: 确保 pinia 实例没有被销毁。 */
if (__DEV__ && !pinia._e.active) {
    throw new Error('Pinia destroyed')
}

const $subscribeOptions: WatchOptions = { deep: true }

// 内部状态 
let isListening: boolean // 异步监听 state 的变化
let isSyncListening: boolean SubscriptionCallback<S>[] = [] // 同步监听
let actionSubscriptions: StoreOnActionListener<Id, S, G, A>[] = [] // 存储 action 变化的订阅回调函数
let debuggerEvents: DebuggerEvent[] | DebuggerEvent // 存储调试事件
const initialState = pinia.state.value[$id] as UnwrapRef<S> | undefined // 从 Pinia 实例的 state 中获取 store 的初始状态

初始化Pinia的state

// 如果不是 options store,并且 Pinia 实例中没有初始状态,则初始化 Pinia 实例中的 state。
if (!isOptionsStore && !initialState && (!__DEV__ || !hot)) { 
    /* istanbul ignore if */ 
    pinia.state.value[$id] = {} 
} 
// 用于存储热模块替换时的 state。
const hotState = ref({} as S)

$patch 方法

// 允许你通过部分 state 对象或 mutation 函数来批量更新 store 的 state。
function $patch( partialStateOrMutator: | _DeepPartial<UnwrapRef<S>> | ((state: UnwrapRef<S>) => void) ): void {
    let subscriptionMutation: SubscriptionCallbackMutation<S>
    // 在更新 state 之前,会暂停监听 state 的变化,以避免触发不必要的回调
    isListening = isSyncListening = false
    if (typeof partialStateOrMutator === 'function') {
      // 一个函数,接收 store 的 state 作为参数,并允许你直接修改 state
      partialStateOrMutator(pinia.state.value[$id] as UnwrapRef<S>)
      subscriptionMutation = {
        type: MutationType.patchFunction,
        storeId: $id,
        events: debuggerEvents as DebuggerEvent[],
      }
    } else {
        // 一个 state 对象,会被合并到 store 的 state 中。
        // 使用 `mergeReactiveObjects` 来合并 state,保持响应性
      mergeReactiveObjects(pinia.state.value[$id], partialStateOrMutator)
      subscriptionMutation = {
        type: MutationType.patchObject,
        payload: partialStateOrMutator,
        storeId: $id,
        events: debuggerEvents as DebuggerEvent[],
      }
    }
    const myListenerId = (activeListener = Symbol())
    // 更新 state 之后 开启state监听
    nextTick().then(() => {
      if (activeListener === myListenerId) {
        isListening = true
      }
    })
    isSyncListening = true
    // 手动触发订阅回调
    triggerSubscriptions(
      subscriptions,
      subscriptionMutation,
      pinia.state.value[$id] as UnwrapRef<S>
    )
}

$reset

$reset: 用于将 store 的 state 重置为初始状态。

  • 对于 Options API 形式的store,它会调用 state 函数来获取初始 state,并使用 $patch 方法来更新 state。
  • 对于 Setup API 形式的 store,开发环境下会抛出一个错误,因为你需要自己实现 $reset 方法。
  const $reset = isOptionsStore
    ? function $reset(this: _StoreWithState<Id, S, G, A>) {
        const { state } = options as DefineStoreOptions<Id, S, G, A>
        const newState: _DeepPartial<UnwrapRef<S>> = state ? state() : {}
        // we use a patch to group all changes into one single subscription
        this.$patch(($state) => {
          // @ts-expect-error: FIXME: shouldn't error?
          assign($state, newState)
        })
      }
    : /* istanbul ignore next */
      __DEV__
      ? () => {
          throw new Error(
            `🍍: Store "${$id}" is built using the setup syntax and does not implement $reset().`
          )
        }
      : noop

$dispose 方法

$dispose: 用于注销 Store,停止 effect scope,移除所有的订阅,并且从 pinia 实例中移除该store。

function $dispose() { 
    scope.stop()
    subscriptions = [] 
    actionSubscriptions = []
    pinia._s.delete($id)
}

getter 定义

Pinia 的 getters 的定义比较分散,并没有一个单独的源码文件来专门定义 getters 的创建过程。 它们主要在以下几个地方被定义和处理:

  1. defineStore 函数 (主要定义的地方):

    • defineStore 函数是定义 store 的入口,无论是 options API 还是 setup API,最终都会通过它来创建 store。
    • 在 defineStore 函数中(特别是 options API 的 store),你可以定义 getters 选项。 这些 getters 会被处理并添加到 store 实例中。
  2. createSetupStore 函数 (setup API 的 store):

    • 当我们使用 setup API 定义 store 时,getter 的定义实际上是在 setup 函数内部完成的,你可以使用 computed 函数来创建 getter。
    • 在 createSetupStore 函数中,会遍历 setup 函数的返回值,如果某个属性是 computed,则会将其识别为 getter,并进行相应的处理(例如,添加到 _hmrPayload.getters 中,以便在 devtools 中显示)。
  3. storeToRefs 函数:

    • storeToRefs 函数用于将 store 的 state 和 getters 转换为 refs,以便在组件中使用。
    • 这个函数会遍历 store 的所有属性,如果是 ref 或者 computed,则会将其转换为 ref,以便在组件中进行响应式地访问。

action 函数

  • action: 一个高阶函数,用于包装 action 函数,以便能够追踪 action 的调用和状态。
  • 它接收一个 action 函数 fn 和 action 的名称 name 作为参数。
  • 返回一个新的函数,该函数在调用原始 action 函数之前和之后,会触发相应的回调函数。
  • 这个函数允许你使用 $onAction 方法来订阅 action 的调用。
const ACTION_MARKER = Symbol('pinia action'); // 一个唯一的 Symbol,用于标记一个 action 是否被 Pinia 包装过。
const ACTION_NAME = Symbol('action name'); // 一个唯一的 Symbol,用于存储 action 的名称。

type _Method = (...args: any[]) => any;
type _ArrayType<T extends any[]> = T extends (infer U)[] ? U : never;

interface MarkedAction<Fn extends _Method> extends Fn {
    [ACTION_MARKER]: true;
    [ACTION_NAME]: string;
}

const action = <Fn extends _Method>(fn: Fn, name: string = ''): Fn => {
    // 如果函数已经被标记为 action,只需要更新它的名称。
    if (ACTION_MARKER in fn) {
        // 确保返回的函数设置了名称。这在 action 被重新定义并赋予新名称时非常重要。
        (fn as unknown as MarkedAction<Fn>)[ACTION_NAME] = name;
        return fn;
    }

    // 创建一个新的包装函数,它拦截原始函数的执行。
    const wrappedAction = function (this: any) {
        // 确保 Pinia 实例在 action 的执行上下文中处于激活状态。
        setActivePinia(pinia);

        const args = Array.from(arguments);

        // 初始化数组以保存 after 和 error 回调。
        const afterCallbackList: Array<(resolvedReturn: any) => any> = [];
        const onErrorCallbackList: Array<(error: unknown) => unknown> = [];

        /**
         * 注册一个回调函数,在 action 成功完成后执行。
         *
         * @param callback - action 完成后要调用的函数。 它接收 action 的解析返回值作为参数。
         */
        function after(callback: _ArrayType<typeof afterCallbackList>) {
            afterCallbackList.push(callback);
        }

        /**
         * 注册一个回调函数,在 action 抛出错误时执行。
         *
         * @param callback - action 发生错误时要调用的函数。 它接收错误作为参数。
         */
        function onError(callback: _ArrayType<typeof onErrorCallbackList>) {
            onErrorCallbackList.push(callback);
        }

        // 在 action 执行之前触发订阅。 这允许订阅者 hook 到 action 的生命周期中。
        // @ts-expect-error: Typescript 不知道 triggerSubscriptions 的类型
        triggerSubscriptions(actionSubscriptions, {
            args, // 传递给 action 的参数。
            name: wrappedAction[ACTION_NAME], // action 的名称。
            store, // Pinia store 实例。
            after, // 注册 action 后回调的函数。
            onError, // 注册错误回调的函数。
        });

        let ret: unknown;

        try {
            // 执行原始的 action 函数。
            // 上下文 (`this`) 被绑定到 store 实例,确保可以正确访问 store 属性。
            // 如果此上下文中的函数且 _id 与 store 的相同,则使用该上下文,否则使用 store
            ret = fn.apply(this && this.$id === $id ? this : store, args);

        } catch (error) {
            // 如果 action 抛出错误,则触发错误订阅并重新抛出错误。
            triggerSubscriptions(onErrorCallbackList, error);
            throw error; // 重新抛出错误以将其传播给调用者。
        }

        // 处理 Promise 返回值(异步 action)。
        if (ret instanceof Promise) {
            return ret
                .then((value) => {
                    // 如果 Promise 成功解析,则触发 after 回调。
                    triggerSubscriptions(afterCallbackList, value);
                    return value; // 返回已解析的值。
                })
                .catch((error) => {
                    // 如果 Promise 被拒绝,则触发错误回调并返回一个被拒绝的 Promise。
                    triggerSubscriptions(onErrorCallbackList, error);
                    return Promise.reject(error); // 返回一个被拒绝的 Promise 以传播错误。
                });
        }

        // 触发同步 action 的 after 回调。
        triggerSubscriptions(afterCallbackList, ret);
        return ret; // 返回同步 action 的结果。
    } as MarkedAction<Fn>;

    // 将包装函数标记为一个 action。
    wrappedAction[ACTION_MARKER] = true;
    // 设置包装 action 的名称。 名称可能会稍后提供。
    wrappedAction[ACTION_NAME] = name;

     // 返回包装的 action,但将类型限制为仅 Fn。
    // 这是因为添加的属性 (ACTION_MARKER、ACTION_NAME) 是内部的,仅通过 `$onAction()` 公开。
    // @ts-expect-error: 我们有意将返回类型限制为仅 Fn
    // 因为所有添加的属性都是内部属性,仅通过 `$onAction()` 公开
    return wrappedAction;
};