Skip to content

第 15 章 状态管理:Pinia 内核

本章要点

  • Pinia 的架构哲学:从 Vuex 的 mutation/action/getter 简化为 state/action/getter
  • createPinia 的实现:一个 effectScope + 一个 reactive Map 构成的轻量容器
  • defineStore 的两种风格:Options Store 与 Setup Store 的内部统一
  • Store 的创建与激活:延迟初始化、单例保证与 Pinia 实例绑定
  • 响应式状态管理:$patch 的智能合并与批量更新优化
  • Store 间的交互:如何在一个 Store 中使用另一个 Store
  • SSR 状态序列化:服务端如何注入初始状态到客户端
  • Pinia 插件系统:扩展每个 Store 的能力

如果说依赖注入是 Vue 生态的"神经网络",那么 Pinia 就是建立在这个网络之上的"大脑"。作为 Vue 官方推荐的状态管理库,Pinia 用不到 2000 行核心代码实现了一个类型安全、支持 SSR、可扩展的全局状态方案。

Vuex 曾是 Vue 2 时代的标配,但它的 mutation 机制饱受争议——mutation 和 action 的边界模糊,mutation 必须同步的限制常常被开发者绕过。Pinia 的回答是:去掉 mutation,让 action 统一处理所有状态变更。这不是简化,而是认识到 mutation 这层抽象并没有带来足够的价值。

15.1 createPinia:状态容器的诞生

源码解析

typescript
// packages/pinia/src/createPinia.ts
export function createPinia(): Pinia {
  const scope = effectScope(true)

  const state = scope.run<Ref<Record<string, StateTree>>>(() =>
    ref<Record<string, StateTree>>({})
  )!

  let _p: Pinia['_p'] = []
  let toBeInstalled: PiniaPlugin[] = []

  const pinia: Pinia = markRaw({
    install(app: App) {
      setActivePinia(pinia)
      pinia._a = app
      app.provide(piniaSymbol, pinia)
      app.config.globalProperties.$pinia = pinia

      toBeInstalled.forEach(plugin => _p.push(plugin))
      toBeInstalled = []
    },

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

    _p,
    _a: null,
    _e: scope,
    _s: new Map<string, StoreGeneric>(),
    state,
  })

  return pinia
}

这段代码信息量极大,逐层拆解:

effectScope(true):独立的副作用作用域

typescript
const scope = effectScope(true)

effectScope(true) 创建一个分离的(detached)作用域。true 参数意味着这个作用域不会被父作用域(组件的 setup)收集。为什么?因为 Pinia 的生命周期是应用级的,不应该跟随任何组件的卸载而销毁。

state:全局状态树

typescript
const state = scope.run(() => ref({}))

所有 Store 的状态都存放在这个单一的 ref 中。key 是 Store 的 id,value 是该 Store 的 state。这使得:

  • DevTools 集成:一个入口就能看到所有状态
  • SSR 序列化JSON.stringify(pinia.state.value) 即可导出所有状态
  • 时间旅行调试:替换整个 state 就能回到任意时间点

_s:Store 实例注册表

typescript
_s: new Map<string, StoreGeneric>()

所有已创建的 Store 实例都注册在这里。这保证了 Store 的单例语义——同一个 id 的 Store 只会创建一次。

markRaw 的妙用

typescript
const pinia: Pinia = markRaw({ ... })

Pinia 实例被 markRaw 标记为永不响应式。为什么?因为 Pinia 对象本身不需要被追踪——它是一个容器,不是状态。如果 Pinia 对象变成响应式的,它内部的 _s(Map)、_p(插件数组)等都会被深度代理,造成不必要的性能开销。

15.2 defineStore:两种风格,一种内核

Options Store

typescript
export const useCounterStore = defineStore('counter', {
  state: () => ({
    count: 0,
    name: 'Counter'
  }),
  getters: {
    doubleCount: (state) => state.count * 2
  },
  actions: {
    increment() {
      this.count++
    }
  }
})

Setup Store

typescript
export const useCounterStore = defineStore('counter', () => {
  const count = ref(0)
  const name = ref('Counter')
  const doubleCount = computed(() => count.value * 2)

  function increment() {
    count.value++
  }

  return { count, name, doubleCount, increment }
})

两种写法产生完全一样的 Store。但内部实现路径不同——Options Store 会被转换为等价的 Setup Store。

defineStore 的实现

typescript
export function defineStore(
  idOrOptions: any,
  setup?: any,
  setupOptions?: any
): StoreDefinition {
  let id: string
  let options: DefineStoreOptions | DefineSetupStoreOptions

  // 重载解析
  const isSetupStore = typeof setup === 'function'
  if (typeof idOrOptions === 'string') {
    id = idOrOptions
    options = isSetupStore ? setupOptions : setup
  } else {
    options = idOrOptions
    id = idOrOptions.id
  }

  function useStore(pinia?: Pinia | null, hot?: StoreGeneric): StoreGeneric {
    // 获取当前组件实例
    const hasContext = hasInjectionContext()
    pinia = pinia || (hasContext ? inject(piniaSymbol, null) : null)

    if (pinia) setActivePinia(pinia)
    pinia = activePinia!

    // 单例检查
    if (!pinia._s.has(id)) {
      // 首次使用,创建 Store
      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
  }

  useStore.$id = id
  return useStore as any
}

注意 defineStore 返回的不是 Store 本身,而是一个 useStore 函数。Store 的创建是延迟的——只有在组件中第一次调用 useStore() 时才会真正创建。

15.3 createOptionsStore:Options 到 Setup 的转换

typescript
function createOptionsStore<Id extends string>(
  id: Id,
  options: DefineStoreOptions<Id, any, any, any>,
  pinia: Pinia
): Store<Id> {
  const { state, actions, getters } = options

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

  let store: Store<Id>

  function setup() {
    if (!initialState) {
      // 首次创建
      pinia.state.value[id] = state ? state() : {}
    }
    // 将 state 的每个属性转换为 ref(toRefs 保持响应性连接)
    const localState = toRefs(pinia.state.value[id])

    return Object.assign(
      localState,
      actions,
      // 将 getters 转换为 computed
      Object.keys(getters || {}).reduce((computedGetters, name) => {
        computedGetters[name] = markRaw(
          computed(() => {
            setActivePinia(pinia)
            const store = pinia._s.get(id)!
            return getters![name].call(store, store)
          })
        )
        return computedGetters
      }, {} as Record<string, ComputedRef>)
    )
  }

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

  return store as any
}

基于 VitePress 构建