为什么Pinia能取代Vuex?源码层揭秘设计精妙之处

#代码星辉·七月创作之星挑战赛#

前言

项目一直在用 Pinia,虽然没看过它内部实现方式,但是大致能猜到,周末刚好有时间,作为一个源码深度爱好者,我们还是来一探究竟吧~

Pinia 是什么我就不再多介绍了,小伙伴们自己去看官网哦。

相关链接:

  • 官网地址:https://pinia.vuejs.org/zh/core-concepts/state.html
  • Github:https://github.com/vuejs/pinia

开始

我们直接去 Github 拉一份最新的源码(这里用的是 pinia@3.0.3 版本):

git clone -b v3 https://github.com/vuejs/pinia.git

在这里插入图片描述

接着我们在根目录安装依赖:

 pnpm install || npm install

安装完依赖后,我们进入到官方提供的一个 demo 项目(playground)中,并启动该项目:

cd ./packages/playground/ && pnpm play

在这里插入图片描述

分析源码之前先贴一张源码的流程图:
在这里插入图片描述

createPinia

我们可以通过该方法获得一个 pinia 实例。

找到 demo 项目的 playground/src/main.ts 入口文件:

import { computed, createApp, markRaw, Ref } from 'vue'
import App from './App.vue'
import { createPinia } from 'pinia'
import { router } from './router'

// 创建一个 pinia 实例
const pinia = createPinia()
// 扩展 pinia 实例
declare module 'pinia' {
  export interface PiniaCustomProperties {
    set route(
      value: RouteLocationNormalizedLoaded | Ref<RouteLocationNormalizedLoaded>
    )
    get route(): RouteLocationNormalized
  }
}
// 自定义插件 
pinia.use(() => ({
  route: computed(() => markRaw(router.currentRoute.value)),
}))
// 使用 app.use 挂载 pinia 实例
const app = createApp(App)
  .use(pinia)
  .use(router)
  // used in counter setup for tests
  .provide('injected', 'global')

app.mount('#app')

可以看到,我们通过 createPinia() 方法获得了一个 pinia 实例,下面我们进入 createPinia 方法源码内部,看它到底做了什么。

找到源码 packages/pinia/src/createPinia.ts 文件的第 10 行:

import { Pinia, PiniaPlugin, piniaSymbol, setActivePinia } from './rootStore'
import { App, effectScope, markRaw, ref, Ref } from 'vue'
import { devtoolsPlugin, registerPiniaDevtools } from './devtools'
import { IS_CLIENT } from './env'
import { StateTree, StoreGeneric } from './types'

/**
 * Creates a Pinia instance to be used by the application
 */
export function createPinia(): Pinia {
  // 创建一个副作用 scope
  const scope = effectScope(true)
  // 存放 pinia 内所有 store 的响应式对象
  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[] = []
  // pinia 实例
  const pinia: Pinia = markRaw({
    install(app: App) { // 提供给 Vue 的插件方法
      // 设置当前活跃的 pinia
      setActivePinia(pinia)
      pinia._a = app
      // 提供全局 provide
      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
}

/**
 * Dispose a Pinia instance by stopping its effectScope and removing the state, plugins and stores. This is mostly
 * useful in tests, with both a testing pinia or a regular pinia and in applications that use multiple pinia instances.
 * Once disposed, the pinia instance cannot be used anymore.
 *
 * @param pinia - pinia instance
 */
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
}

app.use(pinia)

使用 pinia 的时候,我们需要使用 Vue 的 App 实例的 use() 方法去挂载 pinia 实例:

// 使用 app.use 挂载 pinia 实例
const app = createApp(App)
  .use(pinia)
  .use(router)
  // used in counter setup for tests
  .provide('injected', 'global')

app.mount('#app')

Vue 会调用 pinia 实例提供的 install 方法,然后将 app 当参数传入 pinia 的 install 方法:

// ...
export function createPinia(): Pinia {
  // 创建一个副作用 scope
  const scope = effectScope(true)
  // 存放 pinia 内所有 store 的响应式对象
  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[] = []
  // pinia 实例
  const pinia: Pinia = markRaw({
    install(app: App) { // 提供给 Vue 的插件方法
      // 设置当前活跃的 pinia
      setActivePinia(pinia)
      pinia._a = app
      // 提供全局 provide
      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 = []
    },
    // ...
  }
  return pinia
}
//...

install 方法

可以看到, install 方法主要做了以下操作:

  1. 调用 setActivePinia 方法设置当前活跃的 pinia 实例。

    packages/pinia/src/rootStore.ts 文件的第 34 行:

    export let activePinia: Pinia | undefined
    // 设置当前活跃的 pinia 实例
    export const setActivePinia: _SetActivePinia = (pinia) => (activePinia = pinia)
    // 获取当前活跃的 pinia 实例
    export const getActivePinia = () =>
      (hasInjectionContext() && inject(piniaSymbol)) || activePinia
    
  2. 调用 app.provide 方法设置全局 provide,返回 pinia 实例。

  3. 扩展 Vue 的 app 实例,将 pinia 实例挂载到 $pinia 属性。

  4. 开发环境安装开发者插件。

所以在项目中,我们通过以下方式获取 pinia 实例:

import { getCurrentInstance } from 'vue'
import { getActivePinia } from 'pinia'

// 方式一:常用方式
const pinia1 = getActivePinia()
// 方式二:组件内可以直接用 $pinia 属性访问
const currInstance = getCurrentInstance()
const pinia2 = currInstance.proxy.$pinia
console.log(pinia1 === pinia2) // true

小伙伴们自己灵活运用就行。

defineStore 方法

通过该方法我们可以定义一个 Store,它会返回一个创建特定 store 实例的方法 useStore

我们可以找到 demo 项目的 packages/playground/src/stores/counter.ts 文件:

import { acceptHMRUpdate, defineStore } from 'pinia'

const delay = (t: number) => new Promise((r) => setTimeout(r, t))

export const useCounter = defineStore('counter', {
  state: () => ({
    n: 2,
    incrementedTimes: 0,
    decrementedTimes: 0,
    numbers: [] as number[],
  }),

  getters: {
    double: (state) => state.n * 2,
  },

  actions: {
    increment(amount = 1) {
      if (typeof amount !== 'number') {
        amount = 1
      }
      this.incrementedTimes++
      this.n += amount
    },

    changeMe() {
      console.log('change me to test HMR')
    },

    async fail() {
      const n = this.n
      await delay(1000)
      this.numbers.push(n)
      await delay(1000)
      if (this.n !== n) {
        throw new Error('Someone changed n!')
      }

      return n
    },

    async decrementToZero(interval: number = 300, usePatch = true) {
      if (this.n <= 0) return

      while (this.n > 0) {
        if (usePatch) {
          this.$patch({
            n: this.n - 1,
            decrementedTimes: this.decrementedTimes + 1,
          })
        } else {
          this.n -= 1
        }
        await delay(interval)
      }
    },
  },
})

if (import.meta.hot) {
  import.meta.hot.accept(acceptHMRUpdate(useCounter, import.meta.hot))
}

可以看到,调用了 Pinia 提供的 defineStore() 方法定义了一个 useCounter 变量。

接下来我们进入到 defineStore() 方法源码。

找到 packages/pinia/src/store.ts 文件的第 838 行:

// ...
export 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
      >
  // 判断创建方式是 setup 还是 options
  const isSetupStore = typeof setup === 'function'
  // the option store setup will contain the actual options in this case
  options = isSetupStore ? setupOptions : setup
  // 构建 useStore 方法
  function useStore(pinia?: Pinia | null, hot?: StoreGeneric): StoreGeneric {
    // ...
  }

  useStore.$id = id
  
	// 返回 useStore 方法
  return useStore
}
// ...

从源码可以看出,定义 Store 的方式有两种:

  1. setup 方式:以函数方式定义。
  2. options 方式:以对象形式定义。

前面的 packages/playground/src/stores/counter.ts 文件中用的就是 options 方式定义的 Store。

换成 setup 方式为 packages/playground/src/stores/counterSetup.ts 文件:

import { computed, toRefs, reactive, inject } from 'vue'
import { acceptHMRUpdate, defineStore } from 'pinia'

const delay = (t: number) => new Promise((r) => setTimeout(r, t))

export const useCounter = defineStore('counter-setup', () => {
  const state = reactive({
    n: 0,
    incrementedTimes: 0,
    decrementedTimes: 0,
    numbers: [] as number[],
  })

  const injected = inject('injected', 'fallback value')
  console.log('injected (should be global)', injected)

  const double = computed(() => state.n * 2)

  function increment(amount = 1) {
    if (typeof amount !== 'number') {
      amount = 1
    }
    state.incrementedTimes++
    state.n += amount
  }

  function changeMe() {
    console.log('change me to test HMR')
  }

  async function fail() {
    const n = state.n
    await delay(1000)
    state.numbers.push(n)
    await delay(1000)
    if (state.n !== n) {
      throw new Error('Someone changed n!')
    }

    return n
  }

  async function decrementToZero(interval: number = 300) {
    if (state.n <= 0) return

    while (state.n > 0) {
      state.n -= 1
      state.decrementedTimes += 1
      await delay(interval)
    }
  }

  return {
    ...toRefs(state),
    double,
    increment,
    fail,
    changeMe,
    decrementToZero,
  }
})

if (import.meta.hot) {
  import.meta.hot.accept(acceptHMRUpdate(useCounter, import.meta.hot))
}

我们先不介绍两者有啥区别,我们继续源码往下看。

useStore()方法

可以使用 useStore() 方法获取一个特定 Store 的实例。

我们找到 demo 项目的 packages/playground/src/views/AllStores.vue 文件:

<template>
  <p>
    I have a store "{{ userStore.name }}". I have
    {{ cartStore.items.length }} items in the cart.
  </p>
  <div>
    <p>Counter: {{ counterStore.double }} = 2 x {{ counterStore.n }}</p>
    <button @click="counterStore.increment(10)">Increment</button>
    <button @click="counterStore.fail()">Fail</button>
    <button @click="counterStore.decrementToZero(300)">Countdown!</button>
  </div>
</template>

<script setup lang="ts">
import { useUserStore } from '../stores/user'
import { useCartStore } from '../stores/cart'
import { useCounter } from '../stores/counter'

// 获取 User 的 Store 实例
const userStore = useUserStore()
// 获取 Cart 的 Store 实例
const cartStore = useCartStore()
// 获取 Counter 的 Store 实例
const counterStore = useCounter()
</script>

可以看到,当调用了 useUserStore()useCartStore()useCounter() 方法后,最后都会来到 useStore() 方法。

// ...
export 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
      >
  // 判断创建方式是 setup 还是 options
  const isSetupStore = typeof setup === 'function'
  // the option store setup will contain the actual options in this case
  options = isSetupStore ? setupOptions : setup
  // 构建 useStore 方法
  function useStore(pinia?: Pinia | null, hot?: StoreGeneric): StoreGeneric {
    // Vue 是否创建了 App 实例
    const hasContext = hasInjectionContext()
    // 获取 pinia 实例
    pinia =
      // in test mode, ignore the argument provided as we can always retrieve a
      // pinia instance with getActivePinia()
      (__TEST__ && activePinia && activePinia._testing ? null : pinia) ||
      (hasContext ? inject(piniaSymbol, null) : null)
    // 设置当前活跃的 pinia 对象
    if (pinia) setActivePinia(pinia)

    if (__DEV__ && !activePinia) {
      throw new Error(
        `[🍍]: "getActivePinia()" was called but there was no active Pinia. Are you trying to use a store before calling "app.use(pinia)"?\n` +
          `See https://pinia.vuejs.org/core-concepts/outside-component-usage.html for help.\n` +
          `This will fail in production.`
      )
    }
    // 获取当前活跃 pinia 对象
    pinia = activePinia!
    // 判断 pinia 中是否已经定义过该 store 实例
    if (!pinia._s.has(id)) {
      // creating the store registers it in `pinia._s`
      if (isSetupStore) {
        // 创建 setup 方式的 store 实例
        createSetupStore(id, setup, options, pinia)
      } else {
        // 创建 options 方式的 store 实例
        createOptionsStore(id, options as any, pinia)
      }

      /* istanbul ignore else */
      if (__DEV__) {
        // @ts-expect-error: not the right inferred type
        useStore._pinia = pinia
      }
    }
    // 获取当前 store 实例
    const store: StoreGeneric = pinia._s.get(id)!

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

    if (__DEV__ && IS_CLIENT) {
      const currentInstance = getCurrentInstance()
      // save stores in instances to access them devtools
      if (
        currentInstance &&
        currentInstance.proxy &&
        // avoid adding stores that are just built for hot module replacement
        !hot
      ) {
        const vm = currentInstance.proxy
        const cache = '_pStores' in vm ? vm._pStores! : (vm._pStores = {})
        cache[id] = store
      }
    }

    // 返回创建的 store 对象
    return store as any
  }

  useStore.$id = id

  return useStore
}
// ...

可以看到,最主要就是判断 Store 实例创建方式,根据创建方式不同调用不同的方法,当是以 setup 方式创建 Store 实例的时候源码做了啥呢?

当为 setup 创建方式的时候,会调用 createSetupStore() 方法。

createSetupStore() 方法

找到 packages/pinia/src/store.ts 文件createSetupStore() 方法:

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> {
  let scope!: EffectScope
  // 提供给自定义插件的参数
  const optionsForPlugin: DefineStoreOptionsInPlugin<Id, S, G, A> = assign(
    { actions: {} as A },
    options
  )
  // watcher options for $subscribe
  const $subscribeOptions: WatchOptions = { deep: true }

  // internal state
  let isListening: boolean // set to true at the end
  let isSyncListening: boolean // set to true at the end
  let subscriptions: SubscriptionCallback<S>[] = []
  let actionSubscriptions: StoreOnActionListener<Id, S, G, A>[] = []
  let debuggerEvents: DebuggerEvent[] | DebuggerEvent
  // 获取 store 初始化 state 对象
  const initialState = pinia.state.value[$id] as UnwrapRef<S> | undefined
  // 如果是 setup 方式,就初始化一个空的对象
  if (!isOptionsStore && !initialState && (!__DEV__ || !hot)) {
    /* istanbul ignore if */
    pinia.state.value[$id] = {}
  }

  const hotState = ref({} as S)

  // avoid triggering too many listeners
  // https://github.com/vuejs/pinia/issues/1129
  let activeListener: Symbol | undefined
  function $patch(stateMutation: (state: UnwrapRef<S>) => void): void
  function $patch(partialState: _DeepPartial<UnwrapRef<S>>): void
  // patch 方法用于对比并修改 store 实例的 state 对象
  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 = []
    }
    // 函数形式修改 state 对象
    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
    // 触发 state 修改的观察器
    triggerSubscriptions(
      subscriptions,
      subscriptionMutation,
      pinia.state.value[$id] as UnwrapRef<S>
    )
  }
  // options 方式的 Store 才提供 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

  // 销毁当前 store 实例
  function $dispose() {
    scope.stop()
    subscriptions = []
    actionSubscriptions = []
    pinia._s.delete($id)
  }

  /**
   * 封窗 Store 的 action 方法,以便于出发点 $onAction 方法
   * @param fn - action to wrap
   * @param name - name of the action
   */
  const action = <Fn extends _Method>(fn: Fn, name: string = ''): Fn => {
    if (ACTION_MARKER in fn) {
      // we ensure the name is set from the returned function
      ;(fn as unknown as MarkedAction<Fn>)[ACTION_NAME] = name
      return fn
    }

    const wrappedAction = function (this: any) {
      setActivePinia(pinia)
      const args = Array.from(arguments)

      const afterCallbackList: Array<(resolvedReturn: any) => any> = []
      const onErrorCallbackList: Array<(error: unknown) => unknown> = []
      function after(callback: _ArrayType<typeof afterCallbackList>) {
        afterCallbackList.push(callback)
      }
      function onError(callback: _ArrayType<typeof onErrorCallbackList>) {
        onErrorCallbackList.push(callback)
      }

      // @ts-expect-error
      triggerSubscriptions(actionSubscriptions, {
        args,
        name: wrappedAction[ACTION_NAME],
        store,
        after,
        onError,
      })

      let ret: unknown
      try {
        // 触发 action 方法,修改上下文 this 为当前 store 实例,并传入 action 的原始参数
        ret = fn.apply(this && this.$id === $id ? this : store, args)
      } catch (error) {
        // 触发 action 的 error 钩子
        triggerSubscriptions(onErrorCallbackList, error)
        throw error
      }
      // 如果 action 返回的 Promise 异步对象
      if (ret instanceof Promise) {
        return ret
          .then((value) => {
            // 触发 action 的 callback 钩子
            triggerSubscriptions(afterCallbackList, value)
            return value
          })
          .catch((error) => {
            // 触发 action 的 error 钩子
            triggerSubscriptions(onErrorCallbackList, error)
            return Promise.reject(error)
          })
      }
      // 如果 action 返回的不是 Promise 异步对象
      // 触发 action 的 callback 钩子
      triggerSubscriptions(afterCallbackList, ret)
      return ret
    } as MarkedAction<Fn>

    wrappedAction[ACTION_MARKER] = true
    wrappedAction[ACTION_NAME] = name // will be set later
    return wrappedAction
  }

  const _hmrPayload = /*#__PURE__*/ markRaw({
    actions: {} as Record<string, any>,
    getters: {} as Record<string, Ref>,
    state: [] as string[],
    hotState,
  })

  // 初始化 store 实例对象
  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>

  // 将 store 对象变成响应式对象
  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 实例中
  pinia._s.set($id, store as Store)

  const runWithContext =
    (pinia._a && pinia._a.runWithContext) || fallbackRunWithContext

  // 调用传入的 setup 函数,获取当前 store 的 actions 以及 state
  const setupStore = runWithContext(() =>
    pinia._e.run(() => (scope = effectScope()).run(() => setup({ action }))!)
  )!

  // 遍历 setup 函数返回的对象的所有属性
  for (const key in setupStore) {
    const prop = setupStore[key]
    // 是 Ref 类型 && 非 Computed 的属性 || 是 Reactive 的属性
    if ((isRef(prop) && !isComputed(prop)) || isReactive(prop)) {
      // mark it as a piece of state to be serialized
      if (__DEV__ && hot) {
       	//...
      } else if (!isOptionsStore) {
        // in setup stores we must hydrate the state and sync pinia state tree with the refs the user just created
        if (initialState && shouldHydrate(prop)) {
          if (isRef(prop)) {
            prop.value = initialState[key as keyof UnwrapRef<S>]
          } else {
            // probably a reactive object, lets recursively assign
            // @ts-expect-error: prop is unknown
            mergeReactiveObjects(prop, initialState[key])
          }
        }
        // 替换 state 中对应的属性
        pinia.state.value[$id][key] = prop
      }
      // 封装 action 方法
    } else if (typeof prop === 'function') {
      const actionValue = __DEV__ && hot ? prop : action(prop as _Method, key)
      setupStore[key] = actionValue
      optionsForPlugin.actions[key] = prop
    } else if (__DEV__) {
      // ...
    }
  }

  // 将 setup 方法返回的对象合并到 store 中
  assign(store, setupStore)
  // 为了兼容 storeToRefs 方法返回的属性
  assign(toRaw(store), setupStore)

  // 定义 store 的 $state 属性,方便直接修改 state
  Object.defineProperty(store, '$state', {
    get: () => (__DEV__ && hot ? hotState.value : pinia.state.value[$id]),
    set: (state) => {
      /* istanbul ignore if */
      if (__DEV__ && hot) {
        throw new Error('cannot set hotState')
      }
      // 调用 $patch 方法为了触发 state 修改的监听器
      $patch(($state) => {
        // @ts-expect-error: FIXME: shouldn't error?
        assign($state, state)
      })
    },
  }) 

  // 触发所有的自定义插件
  pinia._p.forEach((extender) => {
    /* istanbul ignore else */
    if (__USE_DEVTOOLS__ && IS_CLIENT) {
      // ...
    } else {
      assign(
        store,
        scope.run(() =>
          extender({
            store: store as Store,
            app: pinia._a,
            pinia,
            options: optionsForPlugin,
          })
        )!
      )
    }
  })

	// ...
  return store
}

虽然 createSetupStore() 方法看起来很多,但是核心就干了这些事情:

  1. 初始化 store 对象。
  2. 将当前 store 实例存放至 pinia 实例中。
  3. 调用传入的 setup 函数,获取当前 store 的 actions 以及 state,遍历 setup 函数返回的对象的所有属性并合并到 store 中,封装传入的 actions 方法,用于触发 action 的监听。
  4. 定义 store 的 $state 属性,方便直接修改 state
  5. 创建 $patch 方法:用于对比更新 state,并且触发监听回掉。
  6. 创建 $reset 方法:用于重置 state。
  7. 创建 $dispose 方法:用于销毁当前 store 实例。

上面代码都有注释,这里就不再详细介绍啦~

createOptionsStore() 方法

创建 options 的 Store 实例。

packages/pinia/src/store.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() : {}
    }

    // 调用 state 方法,获取 state 对象
    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,
      // 遍历所有的 getters并封装为 computed
      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>
      )
    )
  }
  
  // 调用 createSetupStore 方法创建 store 实例
  store = createSetupStore(id, setup, options, pinia, hot, true)

  return store as any
}

从源码可以看出,其实 createOptionsStore() 方法最后也是调用了createSetupStore() 方法创建 store 实例 。

这里最后再补充一下 Pinia 库的热更新模块源码。

HMR (热更新)

Pinia 支持热更新,所以你可以编辑你的 store,并直接在你的应用中与它们互动,而不需要重新加载页面,允许你保持当前的 state、并添加甚至删除 state、action 和 getter。

那么它具体是怎么做的呢?

首先得需要你在对应的 Store 中注册下模块的热更新,比如 packages/playground/src/stores/counter.ts

import { acceptHMRUpdate, defineStore } from 'pinia'
export const useCounter = defineStore('counter', {
 	//...
  actions: {
   	// ...
    changeMe() {
      console.log('change me to test HMR')
    },
		//...
})
// 注册该模块的热更新
if (import.meta.hot) {
  import.meta.hot.accept(acceptHMRUpdate(useCounter, import.meta.hot))
}

当我们在开发环境的时候,只有打开了模块的更新后,我们才可以做到不需要重新加载页面,允许你保持当前的 state、并添加甚至删除 state、action 和 getter。

那么 acceptHMRUpdate() 方法到底做了什么呢?

packages/pinia/src/hmr.ts

// ...
export function acceptHMRUpdate<
  Id extends string = string,
  S extends StateTree = StateTree,
  G extends _GettersTree<S> = _GettersTree<S>,
  A = _ActionsTree,
>(initialUseStore: StoreDefinition<Id, S, G, A>, hot: any) {
  // strip as much as possible from iife.prod
  if (!__DEV__) {
    return () => {}
  }
  return (newModule: any) => {
    const pinia: Pinia | undefined = hot.data.pinia || initialUseStore._pinia

    if (!pinia) {
      // this store is still not used
      return
    }

    // preserve the pinia instance across loads
    hot.data.pinia = pinia

    // 获取当前更新模块的 exportName
    for (const exportName in newModule) {
      const useStore = newModule[exportName]
      // 判断是否是 Store 模块
      if (isUseStore(useStore) && pinia._s.has(useStore.$id)) {
        // console.log('Accepting update for', useStore.$id)
        const id = useStore.$id

        if (id !== initialUseStore.$id) {
          console.warn(
            `The id of the store changed from "${initialUseStore.$id}" to "${id}". Reloading.`
          )
          // return import.meta.hot.invalidate()
          return hot.invalidate()
        }
				// 获取热更新前的 store 对象
        const existingStore: StoreGeneric = pinia._s.get(id)!
        if (!existingStore) {
          console.log(`[Pinia]: skipping hmr because store doesn't exist yet`)
          return
        }
        // 调用 useStore 并传入之前已存在的 store 对象,进行属性合并操作。
        useStore(pinia, existingStore)
      }
    }
  }
}

并不建议小伙伴们给每个 Store 都默认加上热更新,给那些经常变动的 Store 加上就可以了,毕竟加上了热更新后会对象合并等一系列操作,项目如果有很多 Store 的话,还是会消耗浏览器性能影响开发体验的。

总结

Pinia 的核心实现要点:

  1. 全局管理:通过 createPinia 创建全局状态容器
  2. Store 创建
    • Options API 最终转换为 Composition API
    • 统一使用 createSetupStore 创建 store
  3. 响应式处理
    • 使用 reactive 创建响应式 store
    • 状态通过 ref/reactive 管理
  4. 插件系统:提供统一的插件扩展点
  5. 热更新:支持开发时模块热替换

Pinia 的设计简洁高效,既保留了 Vuex 的核心概念,又充分利用了 Composition API 的优势,是 Vue 状态管理的现代解决方案。

ok,源码解析就到这里了,Pinia 的更多用法小伙伴们可以自行查询官网或者研究源码去探索一下哦~

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值