Skip to content

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

引言

Pinia 是 Vue.js 的官方状态管理库,被设计为 Vuex 的继任者。它不仅提供了更简洁的 API,还具备完整的 TypeScript 支持、优秀的开发者体验和强大的插件系统。本节将深入分析 Pinia 的核心设计思想和实现原理。

1. Store 设计:defineStore 的实现机制

1.1 defineStore 函数的设计哲学

Pinia 的核心是 defineStore 函数,它支持两种定义方式:Options API 和 Composition API。

typescript
// Options API 风格
const useCounterStore = defineStore('counter', {
  state: () => ({ count: 0 }),
  getters: {
    doubleCount: (state) => state.count * 2
  },
  actions: {
    increment() {
      this.count++
    }
  }
})

// Composition API 风格
const useCounterStore = defineStore('counter', () => {
  const count = ref(0)
  const doubleCount = computed(() => count.value * 2)
  const increment = () => count.value++
  
  return { count, doubleCount, increment }
})

1.2 defineStore 的实现原理

从源码分析可以看出,defineStore 的实现包含以下关键步骤:

typescript
export function defineStore(
  id: any,
  setup?: any,
  setupOptions?: any
): StoreDefinition {
  let options: DefineStoreOptions | DefineSetupStoreOptions
  
  const isSetupStore = typeof setup === 'function'
  options = isSetupStore ? setupOptions : setup

  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)
      }
    }

    return pinia._s.get(id)!
  }

  return useStore
}

1.3 Store 创建的两种模式

Options Store 创建

typescript
function createOptionsStore<Id, S, G, A>(
  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]

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

    const localState = toRefs(pinia.state.value[id])

    return assign(
      localState,
      actions,
      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>
      )
    )
  }

  return createSetupStore(id, setup, options, pinia, hot, true)
}

Setup Store 创建

typescript
function createSetupStore<Id, SS, S, G, A>(
  $id: Id,
  setup: (helpers: SetupStoreHelpers) => SS,
  options: DefineSetupStoreOptions<Id, S, G, A> = {},
  pinia: Pinia,
  hot?: boolean,
  isOptionsStore?: boolean
): Store<Id, S, G, A> {
  let scope!: EffectScope
  
  // 创建 Store 的核心方法
  const $patch = (partialStateOrMutator) => {
    // 状态更新逻辑
  }
  
  const $reset = isOptionsStore
    ? function $reset() {
        const { state } = options as DefineStoreOptions<Id, S, G, A>
        const newState = state ? state() : {}
        this.$patch(($state) => {
          assign($state, newState)
        })
      }
    : noop

  const partialStore = {
    _p: pinia,
    $id,
    $onAction: addSubscription.bind(null, actionSubscriptions),
    $patch,
    $reset,
    $subscribe(callback, options = {}) {
      // 订阅状态变化
    },
    $dispose() {
      scope.stop()
      subscriptions.clear()
      actionSubscriptions.clear()
      pinia._s.delete($id)
    },
  }

  const store: Store<Id, S, G, A> = reactive(partialStore)
  pinia._s.set($id, store as Store)

  // 执行 setup 函数
  const setupStore = runWithContext(() =>
    pinia._e.run(() => (scope = effectScope()).run(() => setup({ action }))!)
  )!

  // 处理 actions 的包装
  for (const key in setupStore) {
    const prop = setupStore[key]
    if ((isRef(prop) && !isComputed(prop)) || isReactive(prop)) {
      // 状态属性
    } else if (typeof prop === 'function') {
      // Action 包装
      const actionValue = __DEV__ && hot ? prop : wrapAction(key, prop)
      setupStore[key] = actionValue
    }
  }

  assign(store, setupStore)
  assign(toRaw(store), setupStore)

  return store
}

2. 状态管理:state、getters、actions 的实现原理

2.1 响应式状态管理

Pinia 基于 Vue 3 的响应式系统构建,所有状态都是响应式的:

typescript
// 状态的响应式处理
const localState = toRefs(pinia.state.value[id])

// 全局状态存储
export function createPinia(): Pinia {
  const scope = effectScope(true)
  const state = scope.run<Ref<Record<string, StateTree>>>(() =>
    ref<Record<string, StateTree>>({})
  )!

  const pinia: Pinia = markRaw({
    install(app: App) {
      setActivePinia(pinia)
      pinia._a = app
      app.provide(piniaSymbol, pinia)
      app.config.globalProperties.$pinia = pinia
    },
    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
}

2.2 Getters 的计算属性实现

Getters 被实现为 Vue 的计算属性,具有缓存和依赖追踪能力:

typescript
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>
)

2.3 Actions 的包装和追踪

Actions 被包装以支持 $onAction 钩子和错误处理:

typescript
const action = <Fn extends _Method>(fn: Fn, name: string = ''): Fn => {
  const wrappedAction = function (this: any) {
    setActivePinia(pinia)
    const args = Array.from(arguments)

    const afterCallbackSet: Set<(resolvedReturn: any) => any> = new Set()
    const onErrorCallbackSet: Set<(error: unknown) => unknown> = new Set()
    
    function after(callback: _SetType<typeof afterCallbackSet>) {
      afterCallbackSet.add(callback)
    }
    function onError(callback: _SetType<typeof onErrorCallbackSet>) {
      onErrorCallbackSet.add(callback)
    }

    // 触发 action 订阅
    triggerSubscriptions(actionSubscriptions, {
      args,
      name: wrappedAction[ACTION_NAME],
      store,
      after,
      onError,
    })

    let ret: unknown
    try {
      ret = fn.apply(this && this.$id === $id ? this : store, args)
    } catch (error) {
      triggerSubscriptions(onErrorCallbackSet, error)
      throw error
    }

    if (ret instanceof Promise) {
      return ret
        .then((value) => {
          triggerSubscriptions(afterCallbackSet, value)
          return value
        })
        .catch((error) => {
          triggerSubscriptions(onErrorCallbackSet, error)
          return Promise.reject(error)
        })
    }

    triggerSubscriptions(afterCallbackSet, ret)
    return ret
  } as MarkedAction<Fn>

  wrappedAction[ACTION_MARKER] = true
  wrappedAction[ACTION_NAME] = name

  return wrappedAction
}

2.4 状态变更的 $patch 机制

typescript
function $patch(
  partialStateOrMutator:
    | _DeepPartial<UnwrapRef<S>>
    | ((state: UnwrapRef<S>) => void)
): void {
  let subscriptionMutation: SubscriptionCallbackMutation<S>
  isListening = isSyncListening = false
  
  if (typeof partialStateOrMutator === 'function') {
    partialStateOrMutator(pinia.state.value[$id] as UnwrapRef<S>)
    subscriptionMutation = {
      type: MutationType.patchFunction,
      storeId: $id,
      events: debuggerEvents as DebuggerEvent[],
    }
  } else {
    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
  
  // 触发订阅回调
  triggerSubscriptions(
    subscriptions,
    subscriptionMutation,
    pinia.state.value[$id] as UnwrapRef<S>
  )
}

3. 插件系统:强大的扩展能力

3.1 插件接口设计

Pinia 的插件系统设计简洁而强大:

typescript
export interface PiniaPlugin {
  (
    context: PiniaPluginContext
  ): Partial<PiniaCustomProperties & PiniaCustomStateProperties> | void
}

export interface PiniaPluginContext<
  Id extends string = string,
  S extends StateTree = StateTree,
  G = _GettersTree<S>,
  A = _ActionsTree,
> {
  pinia: Pinia
  app: App
  store: Store<Id, S, G, A>
  options: DefineStoreOptionsInPlugin<Id, S, G, A>
}

3.2 插件的应用机制

typescript
// 在 createSetupStore 中应用插件
pinia._p.forEach((extender) => {
  if (__USE_DEVTOOLS__ && IS_CLIENT) {
    const extensions = scope.run(() =>
      extender({
        store: store as Store,
        app: pinia._a,
        pinia,
        options: optionsForPlugin,
      })
    )!
    Object.keys(extensions || {}).forEach((key) =>
      store._customProperties.add(key)
    )
    assign(store, extensions)
  } else {
    assign(
      store,
      scope.run(() =>
        extender({
          store: store as Store,
          app: pinia._a,
          pinia,
          options: optionsForPlugin,
        })
      )!
    )
  }
})

3.3 常见插件示例

持久化插件

typescript
function createPersistedState(options = {}) {
  return (context) => {
    const { store } = context
    
    // 从 localStorage 恢复状态
    const stored = localStorage.getItem(`pinia-${store.$id}`)
    if (stored) {
      store.$patch(JSON.parse(stored))
    }
    
    // 监听状态变化并持久化
    store.$subscribe((mutation, state) => {
      localStorage.setItem(`pinia-${store.$id}`, JSON.stringify(state))
    })
  }
}

// 使用插件
const pinia = createPinia()
pinia.use(createPersistedState())

日志插件

typescript
function createLogger() {
  return (context) => {
    const { store } = context
    
    store.$onAction(({ name, args, after, onError }) => {
      console.log(`🚀 Action "${name}" triggered with args:`, args)
      
      after((result) => {
        console.log(`✅ Action "${name}" finished with result:`, result)
      })
      
      onError((error) => {
        console.error(`❌ Action "${name}" failed with error:`, error)
      })
    })
    
    store.$subscribe((mutation, state) => {
      console.log(`📝 State changed:`, mutation)
    })
  }
}

4. DevTools 集成:优秀的开发体验

4.1 DevTools 插件的注册

typescript
export function registerPiniaDevtools(app: App, pinia: Pinia) {
  setupDevtoolsPlugin(
    {
      id: 'dev.esm.pinia',
      label: 'Pinia 🍍',
      logo: 'https://pinia.vuejs.org/logo.svg',
      packageName: 'pinia',
      homepage: 'https://pinia.vuejs.org',
      componentStateTypes,
      app,
    },
    (api) => {
      // 添加时间线层
      api.addTimelineLayer({
        id: MUTATIONS_LAYER_ID,
        label: `Pinia 🍍`,
        color: 0xe5df88,
      })

      // 添加检查器
      api.addInspector({
        id: INSPECTOR_ID,
        label: 'Pinia 🍍',
        icon: 'storage',
        treeFilterPlaceholder: 'Search stores',
        actions: [
          {
            icon: 'content_copy',
            action: () => {
              actionGlobalCopyState(pinia)
            },
            tooltip: 'Serialize and copy the state',
          },
          // 更多操作...
        ],
      })
    }
  )
}

4.2 状态检查和时间旅行

typescript
// 格式化 Store 状态用于检查器
export function formatStoreForInspectorState(
  store: StoreGeneric
): InspectorState {
  const storeState = toRaw(store)
  const state: InspectorState = {}
  
  for (const key in storeState) {
    const value = storeState[key]
    if (typeof value === 'function') continue
    
    state[key] = {
      editable: true,
      value: isRef(value) ? value.value : value,
    }
  }
  
  return state
}

// 时间旅行功能
api.on.editInspectorState((payload) => {
  if (payload.inspectorId === INSPECTOR_ID) {
    const store = pinia._s.get(payload.nodeId)
    if (store) {
      const { path } = payload
      const value = payload.state.value
      
      if (path.length === 1) {
        store.$patch({ [path[0]]: value })
      } else {
        // 深层路径更新
        const obj = path.slice(0, -1).reduce((obj, key) => obj[key], store)
        obj[path[path.length - 1]] = value
      }
    }
  }
})

5. TypeScript 支持:完整的类型推导

5.1 Store 类型定义

typescript
export type Store<
  Id extends string = string,
  S extends StateTree = {},
  G = {},
  A = {},
> = _StoreWithState<Id, S, G, A> &
  UnwrapRef<S> &
  _StoreWithGetters<G> &
  (_ActionsTree extends A ? {} : A) &
  PiniaCustomProperties<Id, S, G, A> &
  PiniaCustomStateProperties<S>

// Store 状态类型
export type _StoreWithState<
  Id extends string,
  S extends StateTree,
  G,
  A
> = StoreProperties<Id> & {
  $state: UnwrapRef<S> & PiniaCustomStateProperties<S>
  $patch(stateMutation: (state: UnwrapRef<S>) => void): void
  $patch(partialState: _DeepPartial<UnwrapRef<S>>): void
  $reset(): void
  $subscribe(
    callback: SubscriptionCallback<S>,
    options?: { detached?: boolean } & WatchOptions
  ): () => void
  $onAction(
    callback: StoreOnActionListener<Id, S, G, A>,
    detached?: boolean
  ): () => void
  $dispose(): void
}

5.2 类型推导的实现

typescript
// 从 Setup Store 中提取状态类型
export type _ExtractStateFromSetupStore<SS> = SS extends undefined | void
  ? {}
  : _ExtractStateFromSetupStore_Keys<SS> extends keyof SS
    ? _UnwrapAll<Pick<SS, _ExtractStateFromSetupStore_Keys<SS>>>
    : never

// 提取状态键
export type _ExtractStateFromSetupStore_Keys<SS> = keyof {
  [K in keyof SS as SS[K] extends _Method | ComputedRef ? never : K]: any
}

// 从 Setup Store 中提取 Getters 类型
export type _ExtractGettersFromSetupStore<SS> = SS extends undefined | void
  ? {}
  : _ExtractGettersFromSetupStore_Keys<SS> extends keyof SS
    ? Pick<SS, _ExtractGettersFromSetupStore_Keys<SS>>
    : never

// 提取 Getters 键
export type _ExtractGettersFromSetupStore_Keys<SS> = keyof {
  [K in keyof SS as SS[K] extends ComputedRef ? K : never]: any
}

// 从 Setup Store 中提取 Actions 类型
export type _ExtractActionsFromSetupStore<SS> = SS extends undefined | void
  ? {}
  : _ExtractActionsFromSetupStore_Keys<SS> extends keyof SS
    ? Pick<SS, _ExtractActionsFromSetupStore_Keys<SS>>
    : never

// 提取 Actions 键
export type _ExtractActionsFromSetupStore_Keys<SS> = keyof {
  [K in keyof SS as SS[K] extends _Method ? K : never]: any
}

5.3 storeToRefs 的类型安全

typescript
export function storeToRefs<SS extends StoreGeneric>(
  store: SS
): StoreToRefs<SS> {
  // 在开发环境中检查是否为 Store 实例
  if (__DEV__ && !isStore(store)) {
    throw new Error(
      `"storeToRefs()" is only usable on stores, not on plain objects. Received "${typeof store}".`
    )
  }

  const refs = {} as StoreToRefs<SS>
  
  for (const key in store) {
    const value = store[key]
    if (isRef(value) || isReactive(value)) {
      refs[key as keyof StoreToRefs<SS>] = toRef(store, key)
    }
  }

  return refs
}

// StoreToRefs 类型定义
export type StoreToRefs<SS extends StoreGeneric> = ToRefs<
  StoreState<SS> & StoreGetters<SS> & PiniaCustomStateProperties<StoreState<SS>>
>

6. 核心特性深度解析

6.1 模块化设计

Pinia 支持多个独立的 Store,每个 Store 都有自己的命名空间:

typescript
// 用户 Store
const useUserStore = defineStore('user', {
  state: () => ({
    id: null,
    name: '',
    email: ''
  }),
  actions: {
    async fetchUser(id) {
      const response = await api.getUser(id)
      this.$patch(response.data)
    }
  }
})

// 购物车 Store
const useCartStore = defineStore('cart', {
  state: () => ({
    items: [],
    total: 0
  }),
  getters: {
    itemCount: (state) => state.items.length,
    totalPrice: (state) => state.items.reduce((sum, item) => sum + item.price, 0)
  },
  actions: {
    addItem(item) {
      this.items.push(item)
      this.updateTotal()
    },
    updateTotal() {
      this.total = this.totalPrice
    }
  }
})

// Store 之间的组合使用
const useOrderStore = defineStore('order', () => {
  const userStore = useUserStore()
  const cartStore = useCartStore()
  
  const createOrder = async () => {
    if (!userStore.id) {
      throw new Error('User not logged in')
    }
    
    const orderData = {
      userId: userStore.id,
      items: cartStore.items,
      total: cartStore.total
    }
    
    const response = await api.createOrder(orderData)
    cartStore.$reset() // 清空购物车
    return response.data
  }
  
  return { createOrder }
})

6.2 SSR 支持

Pinia 提供了完整的服务端渲染支持:

typescript
// 服务端
import { createPinia } from 'pinia'
import { createSSRApp } from 'vue'

export function createApp() {
  const app = createSSRApp(App)
  const pinia = createPinia()
  
  app.use(pinia)
  
  return { app, pinia }
}

// 在服务端预取数据
export async function renderPage(url) {
  const { app, pinia } = createApp()
  
  // 预取数据
  const userStore = useUserStore(pinia)
  await userStore.fetchUser()
  
  const html = await renderToString(app)
  const state = JSON.stringify(pinia.state.value)
  
  return { html, state }
}

// 客户端水合
const { app, pinia } = createApp()

// 恢复服务端状态
if (window.__PINIA_STATE__) {
  pinia.state.value = window.__PINIA_STATE__
}

app.mount('#app')

6.3 热模块替换 (HMR)

typescript
// 支持热更新的 Store 定义
const useCounterStore = defineStore('counter', {
  state: () => ({ count: 0 }),
  actions: {
    increment() {
      this.count++
    }
  }
})

// 启用热更新
if (import.meta.hot) {
  import.meta.hot.accept(acceptHMRUpdate(useCounterStore, import.meta.hot))
}

// acceptHMRUpdate 的实现
export function acceptHMRUpdate(
  initialUseStore: StoreDefinition,
  hot: any
) {
  return (newModule: any) => {
    const pinia = getActivePinia()
    if (!pinia) return
    
    const id = initialUseStore.$id
    const oldStore = pinia._s.get(id)
    if (!oldStore) return
    
    const newStore = newModule[initialUseStore.name] || newModule.default
    if (!newStore) return
    
    // 保存当前状态
    const oldState = oldStore.$state
    
    // 创建新的 Store
    newStore(pinia, oldStore)
    
    // 恢复状态
    oldStore.$patch(oldState)
  }
}

7. 性能优化策略

7.1 按需响应式

Pinia 只对实际使用的状态创建响应式引用:

typescript
// 使用 storeToRefs 避免失去响应性
const store = useCounterStore()
const { count, doubleCount } = storeToRefs(store)
const { increment } = store // actions 不需要 toRefs

// 错误用法 - 会失去响应性
const { count, doubleCount, increment } = store

7.2 计算属性缓存

Getters 基于 Vue 的计算属性实现,具有自动缓存:

typescript
const useExpensiveStore = defineStore('expensive', {
  state: () => ({
    items: []
  }),
  getters: {
    // 只有当 items 变化时才重新计算
    expensiveComputation: (state) => {
      console.log('Computing...')
      return state.items.reduce((sum, item) => {
        return sum + complexCalculation(item)
      }, 0)
    }
  }
})

7.3 批量更新

使用 $patch 进行批量状态更新:

typescript
const store = useUserStore()

// 低效 - 触发多次响应式更新
store.name = 'John'
store.email = 'john@example.com'
store.age = 30

// 高效 - 批量更新
store.$patch({
  name: 'John',
  email: 'john@example.com',
  age: 30
})

// 或使用函数形式
store.$patch((state) => {
  state.name = 'John'
  state.email = 'john@example.com'
  state.age = 30
})

8. 最佳实践

8.1 Store 组织结构

typescript
// stores/index.ts
export { useUserStore } from './user'
export { useCartStore } from './cart'
export { useOrderStore } from './order'

// stores/user.ts
export const useUserStore = defineStore('user', () => {
  // 状态
  const user = ref(null)
  const isLoggedIn = computed(() => !!user.value)
  
  // Actions
  const login = async (credentials) => {
    const response = await authAPI.login(credentials)
    user.value = response.data.user
    return response
  }
  
  const logout = () => {
    user.value = null
    // 清理其他相关状态
  }
  
  return {
    // 状态
    user: readonly(user),
    isLoggedIn,
    
    // Actions
    login,
    logout
  }
})

8.2 错误处理

typescript
const useApiStore = defineStore('api', () => {
  const loading = ref(false)
  const error = ref(null)
  
  const fetchData = async (url) => {
    loading.value = true
    error.value = null
    
    try {
      const response = await fetch(url)
      if (!response.ok) {
        throw new Error(`HTTP ${response.status}: ${response.statusText}`)
      }
      return await response.json()
    } catch (err) {
      error.value = err.message
      throw err
    } finally {
      loading.value = false
    }
  }
  
  return {
    loading: readonly(loading),
    error: readonly(error),
    fetchData
  }
})

8.3 测试策略

typescript
// 测试 Store
import { createPinia, setActivePinia } from 'pinia'
import { useCounterStore } from '@/stores/counter'

describe('Counter Store', () => {
  beforeEach(() => {
    setActivePinia(createPinia())
  })
  
  it('increments count', () => {
    const store = useCounterStore()
    expect(store.count).toBe(0)
    
    store.increment()
    expect(store.count).toBe(1)
  })
  
  it('resets count', () => {
    const store = useCounterStore()
    store.count = 10
    
    store.$reset()
    expect(store.count).toBe(0)
  })
})

总结

Pinia 作为新一代状态管理库,在设计上体现了以下核心思想:

  1. 简洁性:API 设计简洁直观,学习成本低
  2. 类型安全:完整的 TypeScript 支持,提供优秀的开发体验
  3. 模块化:支持多个独立 Store,便于大型应用的状态管理
  4. 可扩展性:强大的插件系统,支持各种扩展需求
  5. 开发友好:优秀的 DevTools 集成和调试支持
  6. 性能优化:基于 Vue 3 响应式系统,具有良好的性能表现

通过深入理解 Pinia 的实现原理,我们可以更好地利用其特性构建高质量的 Vue.js 应用,同时也为我们设计自己的状态管理方案提供了宝贵的参考。


微信公众号二维码