Skip to content

Pinia - 新一代状态管理库的核心设计思想

引言

Pinia 是 Vue 官方的状态管理库,作为 Vuex 的替代方案。它提供了更简洁的 API、完整的 TypeScript 支持和可扩展的插件系统。其设计核心是模块化扁平化,取消了 Vuex 中的 mutations 和多层 modules

本节将深入其源码,分析 Pinia 是如何通过 defineStore 和一个全局 ref 来实现其状态管理系统的。


1. createPinia:全局状态容器

Pinia 的工作流程始于 createPinia()。这个函数的核心职责是创建 Pinia 实例,这个实例是所有 store 的管理者。

typescript
// packages/pinia/src/createPinia.ts
export function createPinia(): Pinia {
  // 1. 【核心】创建一个 EffectScope,
  //    并在其内部创建一个全局的 ref 对象,用于存储“所有” store 的 state
  const scope = effectScope(true)
  const state = scope.run<Ref<Record<string, StateTree>>>(() =>
    ref<Record<string, StateTree>>({}) // 所有的 state 都存在这里
  )!

  const pinia: Pinia = {
    // 2. install:
    //    将 pinia 实例和这个全局 state
    //    通过 provide 提供给所有子组件
    install(app: App) {
      setActivePinia(pinia)
      app.provide(piniaSymbol, pinia)
      // ...
    },
    
    _e: scope, // EffectScope 实例
    _s: new Map<string, StoreGeneric>(), // Store 实例的缓存 Map
    state, // 全局 state = ref({})
  }

  return pinia
}

Pinia 的第一个核心设计:Pinia 实例本身非常轻量。它只管理一个全局 refpinia.state)用于存放所有 state,以及一个 Mappinia._s)用于缓存 store 实例


2. defineStore:“Store 定义”函数

defineStore创建 store 实例。它是一个“工厂”,用于定义一个 store 的“配方”(state, getters, actions),并返回一个 useStore 函数。

typescript
// packages/pinia/src/store.ts
export function defineStore(
  id: string,
  setup: // (Options API 对象 或 Setup 函数),
  setupOptions?: any
): StoreDefinition {
  
  const isSetupStore = typeof setup === 'function'

  // defineStore 只返回一个“useStore”函数
  function useStore(pinia?: Pinia | null): StoreGeneric {
    // ... (具体实现在下一节)
  }

  return useStore
}

3. useStore:实例的“获取与创建”

useStore (例如 useCounterStore()) 才是 Pinia 的执行核心。它负责获取创建 store 实例。

typescript
// defineStore 内部返回的 useStore 函数
function useStore(pinia?: Pinia | null): StoreGeneric {
  // 1. 【获取 Pinia】
  //    通过 inject(piniaSymbol) 找到全局 Pinia 实例
  pinia = pinia || (hasInjectionContext() ? inject(piniaSymbol) : null)
  setActivePinia(pinia)
  pinia = activePinia!

  // 2. 【检查缓存】
  //    检查 pinia._s (Map 缓存) 中是否“已经”创建过这个 store?
  if (!pinia._s.has(id)) {
    // 3. 【创建 Store】
    //    如果缓存中没有,就创建它
    if (isSetupStore) {
      // 路径 A: Setup Store ( () => { ... } )
      createSetupStore(id, setup, options, pinia)
    } else {
      // 路径 B: Options Store ( { state: ... } )
      createOptionsStore(id, options as any, pinia)
    }
  }

  // 4. 【返回实例】
  //    从缓存中获取 store 实例并返回
  return pinia._s.get(id)!
}

Pinia 的第二个核心设计store单例的。useStore() 在第一次被调用时创建 store,并将其缓存。后续所有调用始终返回同一个实例


4. createOptionsStore vs createSetupStore:统一实现

Pinia 巧妙地将两种 API 风格统一到了一个实现上。

createOptionsStore (Options API)

createOptionsStore 只是一个“转换器”。它接收 Options API(state, getters, actions),将其转换为一个 setup 函数,然后调用 createSetupStore

typescript
// packages/pinia/src/store.ts
function createOptionsStore(
  id: string,
  options: DefineStoreOptions,
  pinia: Pinia,
) {
  const { state, getters, actions } = options

  function setup() {
    // 1. 【State】
    //    执行 state() 函数,将其返回值注册到“全局 state”
    pinia.state.value[id] = state ? state() : {}
    //    创建 state 的本地引用
    const localState = toRefs(pinia.state.value[id])

    // 2. 【Getters】
    //    将 getters 转换为 computed
    const computedGetters = Object.keys(getters || {}).reduce(
      (gettersMap, name) => {
        gettersMap[name] = markRaw(
          computed(() => {
            // ...
            return getters![name].call(store, store)
          })
        )
        return gettersMap
      },
      {}
    )

    // 3. 【返回】
    //    返回“转换”后的 Setup Store 结构
    return assign(
      localState, // { count: Ref<0> }
      actions,    // { increment: Function }
      computedGetters // { double: ComputedRef<0> }
    )
  }

  // 4. 【调用】
  //    最终还是调用 createSetupStore
  return createSetupStore(id, setup, options, pinia)
}

createSetupStore (Setup API)

createSetupStore真正store 构造器。

typescript
// packages/pinia/src/store.ts
function createSetupStore(
  $id: string,
  setup: Function,
  options: DefineSetupStoreOptions,
  pinia: Pinia,
): Store {
  
  // 1. 【创建基础 Store】
  //    创建一个响应式对象,包含 $id, $patch, $onAction 等
  const store: Store = reactive(partialStore)

  // 2. 【缓存 Store】
  //    在执行 setup 之前,先将“空壳” store 放入缓存
  //    (这允许在 setup 内部调用 useStore() 引用自己)
  pinia._s.set($id, store)

  // 3. 【执行 Setup】
  //    在 EffectScope 中执行用户传入的 setup() 函数
  const setupStore = pinia._e.run(() => 
    (scope = effectScope()).run(() => setup())
  )!

  // 4. 【分离属性】
  //    遍历 setup() 的返回值
  for (const key in setupStore) {
    const prop = setupStore[key]

    if ((isRef(prop) && !isComputed(prop)) || isReactive(prop)) {
      // 【State】
      // 如果是 ref 或 reactive,它就是“状态”
      // (在 Options Store 中,这里会被注册到 pinia.state.value)
      
    } else if (isRef(prop) && isComputed(prop)) {
      // 【Getters】
      // (无需特殊处理,它已经是 computed)
      
    } else if (typeof prop === 'function') {
      // 【Actions】
      //    如果是函数,它就是“Action”
      //    【核心】用 wrapAction 包装它
      setupStore[key] = wrapAction(key, prop)
    }
  }

  // 5. 【合并】
  //    将 setup() 返回的(已包装的)属性
  //    合并到基础 store 实例上
  assign(store, setupStore)
  
  return store
}

5. storeToRefs:为什么需要它?

store 实例上的 stategetters 都是响应式的(ref / computed)。如果直接解构,会丢失响应性

javascript
const store = useCounterStore()
// 错误!count 是一个“值”(0),不是 ref
const { count, doubleCount } = store

storeToRefs (packages/pinia/src/storeToRefs.ts) 是一个辅助函数,它遍历 storestategetters,并为它们创建 toRef 引用

typescript
// packages/pinia/src/storeToRefs.ts
export function storeToRefs<SS extends StoreGeneric>(
  store: SS
): StoreToRefs<SS> {
  const refs = {} as StoreToRefs<SS>
  
  for (const key in store) {
    const value = store[key]
    
    // 【关键】
    // 只转换 ref, reactive 和 computed 属性
    // (它会“跳过” actions 和 $patch 等函数)
    if (isRef(value) || isReactive(value)) {
      refs[key as keyof StoreToRefs<SS>] = 
        toRef(store, key) // 创建一个链接到 store 的 ref
    }
  }

  return refs
}

6. 插件与订阅 ($patch, $onAction)

Pinia 移除了 mutations,但提供了更灵活的“拦截”机制。

$patch:状态“拦截”

$patch 允许你批量修改 state$patch 的实现就是直接修改 pinia.state.value[id] 这个全局状态对象。

typescript
// store 上的 $patch 方法 (简化)
function $patch(partialStateOrMutator) {
  // 触发订阅 (before)
  triggerSubscriptions(subscriptions, ...)

  if (typeof partialStateOrMutator === 'function') {
    // 1. $patch(state => { ... })
    //    直接操作“全局 state”中的那片区域
    partialStateOrMutator(pinia.state.value[$id])
  } else {
    // 2. $patch({ ... })
    //    合并对象到“全局 state”
    mergeReactiveObjects(pinia.state.value[$id], partialStateOrMutator)
  }

  // 触发订阅 (after)
  triggerSubscriptions(subscriptions, ...)
}

store.$subscribe(callback) 就是在订阅 pinia.state.value[$id] 的变更。

$onAction:Actions“拦截”

$onAction 是通过 wrapAction 实现的。createSetupStore 在遍历 setup 返回值时,会用 wrapAction 包装所有函数:

typescript
// packages/pinia/src/actions.ts (简化)
function wrapAction(name: string, action: Function): Function {
  return function (this: any) {
    // ...
    const args = Array.from(arguments)
    
    // 1. 【发布“before”】
    //    触发 $onAction 订阅
    triggerSubscriptions(actionSubscriptions, {
      name,
      store,
      args,
      after, // 允许“after”回调
      onError, // 允许“onError”回调
    })

    let ret: unknown
    try {
      // 2. 【执行 Action】
      ret = action.apply(this, args)
    } catch (error) {
      // 3. 【发布“error”】
      triggerSubscriptions(onErrorCallbackSet, error)
      throw error
    }

    if (ret instanceof Promise) {
      // 4. 【处理异步 Action】
      return ret
        .then((value) => {
          // 5. 【发布“after”】
          triggerSubscriptions(afterCallbackSet, value)
          return value
        })
        .catch(...)
    }

    // 6. 【发布“after”】 (同步 Action)
    triggerSubscriptions(afterCallbackSet, ret)
    return ret
  }
}

总结

Pinia 的设计基于 Vue 3 的响应式系统,而不是另建一套:

  1. 全局状态 (createPinia):所有 storestate 共享同一个全局 ref (pinia.state)。
  2. defineStore (工厂):返回一个 useStore 函数。
  3. useStore (单例):负责 inject 全局 pinia,并通过 pinia._s (Map 缓存) 获取或创建 store 实例。
  4. 统一实现createOptionsStore 只是一个转换器,它将 { state, getters, actions } 转换为 setup 函数,最终统一由 createSetupStore 处理。
  5. createSetupStore (构造器)
    • State:注册到 pinia.state.value[id]
    • Getters:被转换为 computed
    • Actions:被 wrapAction 包装,以支持 onAction 钩子。
  6. 插件 (plugin.use):在 createSetupStore 期间,store 实例被创建后,会遍历 pinia._p (插件数组) 并执行它们,允许插件store 实例上添加新属性(如 $persistedState)。
  7. 解构storeToRefs必需的,因为它为 stategetters 创建了 toRef,保持了响应式连接。

微信公众号二维码

Last updated: