Libon

Pinia 是如何被实现的

#JavaScript#pinia
学习 Vue3 状态管理库 Pinia 的实现原理

ToC

Init

Pinia 官方文档中的 Getting StartedWhat is Pinia? 一节中,我们可以看到 Pinia 的基本使用方式的代码,比如全局注册:

import { createApp } from 'vue'
import { createPinia } from 'pinia'
import App from './App.vue'

const pinia = createPinia() // 创建一个应用级 pinia 的实例
const app = createApp(App)

app.use(pinia) // 将 pinia 实例挂载到 app 上
app.mount('#app')

定义 store

import { defineStore } from 'pinia'

export const useCounterStore = defineStore('counter', {
  state: () => {
    return { count: 0 }
  },
  // state: () => ({ count: 0 })
  actions: {
    increment() {
      this.count++
    },
  },
})

使用 store

<script setup>
import { useCounterStore } from '@/stores/counter'
const counter = useCounterStore()
counter.count++
counter.$patch({ count: counter.count + 1 })
counter.increment()
</script>
<template>
  <div>Current Count: {{ counter.count }}</div>
</template>

从基本的使用方法来看,主要的 API 其实就只有两个:createPinia defineStore,那么我们就从这两个 API 入手,来看看 Pinia 是如何被实现的。

createPinia

为了降低理解成本,后续的代码都会去除 ts 类型的定义,只保留核心及上下文逻辑,同时因为会增加很多的注释来解释代码,所以在代码中也会增加很多的空行,避免代码过于密集,影响阅读

/**
 * 必须调用setActivePinia来处理诸如 "fetch"、"setup"、"serverPrefetch" 等函数顶部的SSR
 */
export let activePinia

/**
 * 设置或清空 active pinia, 在SSR和内部调用 action 和 getter 时使用
 *
 * @param pinia - Pinia 实例
 */
export const setActivePinia = (pinia) => (activePinia = pinia)

// createPinia 实际上是一个工厂函数,每次调用的时候都会返回一个新的 pinia 实例对象
export function createPinia() {
  // effectScope 用于创建一个新的 effect scope, 用于隔离副作用
  // 其本身是一个比较高级的API,可以参考这个 RFC: https://github.com/vuejs/rfcs/blob/master/active-rfcs/0041-reactivity-effect-scope.md
  // 在正常的项目开发中基本用不到,但是在开发一些库或者依赖的时候可以更方便地收集依赖在运行时增加的副作用函数或响应式对象变量
  // 在实例被销毁的时候,会自动清理掉这些副作用函数和响应式对象
  // 它只接收一个 `detached` 参数,用于表示是否将这些副作用函数和响应式对象从当前的 effect scope 中独立出来,而不是和当前渲染的实例绑定,这样做的好处是在某个时机可以手动一次性清理掉这些副作用函数和响应式对象
  const scope = effectScope(true)

  // 在这里,我们可以检查 window object 的状态,如果Vue3 SSR有类似的情况,可以直接设置它
  const state = scope.run(() => ref({}))

  // 在 pinia 实例注册到 vue app 中的时候需要注册的插件列表
  let _p = []

  // plugins added before calling app.use(pinia)
  // 在调用app.use(pinia)之前添加的插件,像这样👇:
  /**
   * const app = createApp(App)
   * const pinia = createPinia()
   * pinia.use(xxx) // 👈 xxx 就会被放到这里
   * app.use(pinia)
   */
  let toBeInstalled = []

  // pinia 则是创建出来的 pinia 实例
  // markRaw 则是将一个对象标记为不可响应的,底层实现就是将对象的 __v_skip 属性设置为 true
  // 这样即便它被 reactive/ref 包装以后,它也仍然是一个普通对象 👇:
  /**
   * const pinia = reactive(createPinia())
   * console.log(isReactive(pinia)) // false
   */
  const pinia: Pinia = markRaw({
    // app.use(pinia) 的时候会调用 install 方法
    install(app: App) {

      // 这允许在安装pinia的插件后,在组件设置之外调用 useStore()
      setActivePinia(pinia)

      // 如果不是 Vue2 才会执行这里的逻辑
      // Vue2 是通过全局 mixin 的方式来实现的,文章后面会讲到
      if (!isVue2) {

        // 将当前 vue app 实例缓存到 pinia._a,以便于能找到正确的上下文
        // 因为 pinia 像 vue app 一样都是可以创建多个的
        pinia._a = app

        // 将pinia实例挂载到 app.provide 上,以便于在组件中 defineStore 返回的函数中通过 inject 获取注册的数据
        app.provide(piniaSymbol, pinia)

        // 注册一个全局属性,以便于 OptionsAPI 中可以通过 this.$pinia 访问到 pinia 实例
        // 或者直接在模板中通过 $pinia.state.value.xxx 来访问 xxx 模块的数据,示例如下:
        /**
         * <div>computer: {{ $pinia.state.value.computer }}</div>
         */
        app.config.globalProperties.$pinia = pinia

        // 如果不是 SSR 环境并且是开发环境则注册 devtools 面板
        // 关于 vue-devtools 的使用暂且按下不表,有兴趣的话可以后期重新开一篇文章来讲解
        if (__USE_DEVTOOLS__ && IS_CLIENT) {
          registerPiniaDevtools(app, pinia)
        }

        // 把注册前使用的 pinia 插件拷贝到 _p 将要注册的插件数组中
        toBeInstalled.forEach((plugin) => _p.push(plugin))

        // 清空注册前使用的 pinia 插件数组,避免重复注册以及 AO 内存泄露
        toBeInstalled = []
      }
    },

    // use 方法和 vue app 一样,用于注册 pinia 插件
    use(plugin) {
      // 如果还没有 .use(pinia) 则先把插件存储到 toBeInstalled 数组中
      if (!this._a && !isVue2) {
        toBeInstalled.push(plugin)
      } else {
        // 如果已经 .use(pinia) 则直接注册插件
        _p.push(plugin)
      }

      // 返回 this,方便链式调用
      return this
    },

    _p, // plugins
    _a: null, // app
    _e: scope, // effectScope
    _s: new Map(), // store map, 方便通过 id 来获取到对应的 store
    state, // 实际上是一个 Ref<Record<string, Record<string | number | symbol, any>>> 类型, 和 _s 是同一类型的数据
  })

  // pinia devtools依赖于仅限开发的功能,因此除非使用Vue的开发构建,否则不能强制使用这些功能。避免使用像IE11这样的旧浏览器。
  if (__USE_DEVTOOLS__ && typeof Proxy !== 'undefined') {
    pinia.use(devtoolsPlugin)
  }

  // 返回实例对象,这样就可以 .use(pinia) 了
  return pinia
}

既然提到了创建,那就顺便看看销毁pinia的 disposePinia 方法吧,虽然这个方法非常少用,大多数只在做测试的时候才会用到,但是了解一下也是好的,万一什么时候就用到了呢?

/**
 * 通过停止其 effectScope 并删除状态、插件和存储来处理 Pinia 实例。这在测试 pinia 或常规 pinia 的测试中以及在使用多个 pinia 实例的应用中都非常有用。
 *
 * @param pinia - pinia 实例
 */
export function disposePinia(pinia: Pinia) {
  pinia._e.stop() // 停止 effectScope, 销毁收集到的所有副作用
  pinia._s.clear() // 清空 state map
  pinia._p.splice(0) // 清空 plugins
  pinia.state.value = {} // 清空 state
  pinia._a = null // 置空 app
}

defineStore

相比之下,defineStore 的代码会多很多,因为 pinia 要处理对象 state|actions|getters,而且 defineStore 函数不仅支持传入一个对象,还支持传入一个函数,在实现上也会有一些差异,所以我们分开来看,但是为了避免解析和最新版本的代码出现对应不上的问题,所以在查看代码的对照源代码的时候最好是使用指定 commit 的代码,比如这次用的 commit 就是 fix: support webpack minification

core

export function defineStore(
  idOrOptions,
  setup,
  setupOptions
): StoreDefinition {
  let id
  let options

  // 判断第二个参数是否是函数类型的 store
  const isSetupStore = typeof setup === 'function'

  // 判断第一个参数是不是字符串, 如果是则表示是 id
  if (typeof idOrOptions === 'string') {
    id = idOrOptions
    // 如果第一个参数是字符串, 那么再判断第二个参数是不是函数
    // 如果是函数, 则第三个参数才是真正的 options
    options = isSetupStore ? setupOptions : setup
  } else {
    options = idOrOptions
    id = idOrOptions.id

    // 如果传递的是一个对象, 但是对象上没有设置 id,
    // 或者传入了一个函数, 但是函数上没有增加 id 属性则抛出警告
    if (__DEV__ && typeof id !== 'string') {
      throw new Error(
        `[🍍]: "defineStore()" must be passed a store id as its first argument.`
      )
    }
  }

  // 调用 defineStore 以后会返回这个函数,
  function useStore(pinia?: Pinia | null, hot?: StoreGeneric): StoreGeneric {
    // hasInjectionContext() 的实现在 【Vue3 project/inject 源码实现】 一文中有过介绍, 感兴趣可自行查阅
    const hasContext = hasInjectionContext()
    pinia =
      // 在测试模式下,忽略提供的参数,因为我们始终可以使用 getActivePinia() 检索 pinia 实例
      (__TEST__ && activePinia && activePinia._testing ? null : pinia) ||
      (hasContext ? inject(piniaSymbol, null) : null)

    // 设置成当前激活的 pinia 实例
    if (pinia) setActivePinia(pinia)

    // 如果没有通过 activePinia 获取到 pinia 实例则表示不是在 setup() 中使用的, 给出警告
    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 = activePinia!

    // 如果没有定义过这个 id 的模块就定义它
    if (!pinia._s.has(id)) {
      // 在注册了对应的 store 的时候将其保存到 _s 中
      // 两个创建的函数放在下文解析
      if (isSetupStore) {
        createSetupStore(id, setup, options, pinia)
      } else {
        createOptionsStore(id, options as any, pinia)
      }

      // istanbul 是一个检查 JS 代码覆盖率的工具, 这里是告诉它忽略检测 else
      /* istanbul ignore else */
      if (__DEV__) {
        useStore._pinia = pinia
      }
    }

    // 获取到对应的 store 对象
    const store: StoreGeneric = pinia._s.get(id)!

    // 在开发期间注册热更新模块的替换逻辑
    if (__DEV__ && hot) {
      const hotId = '__hot:' + id
      // 注册一个包含已变更数据/状态的 store 去覆盖原有的 store, 这就是热更新的原理
      const newStore = isSetupStore
        ? createSetupStore(hotId, setup, options, pinia, true)
        : createOptionsStore(hotId, assign({}, options) as any, pinia, true)

      // 应用热更新的数据
      hot._hotUpdate(newStore)

      // 从缓存中清除状态属性和存储
      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 &&
        // 避免添加专为热模块更换而构建的存储
        !hot
      ) {
        // 获取到当前组件的实例详情
        const vm = currentInstance.proxy
        // 在实例上临时增加 store 的引用, 具体作用不详
        const cache = '_pStores' in vm ? vm._pStores! : (vm._pStores = {})
        cache[id] = store
      }
    }

    // StoreGeneric 无法匹配到 Store
    return store as any
  }

  // 给闭包函数增加 id, 方便 vue-devtools 溯源
  useStore.$id = id

  return useStore
}

createOptionsStore

我们先看一下创建 Options Store 的方法:

const { assign } = Object

function createOptionsStore(
  id,
  options,
  pinia,
  hot
) {
  const { state, actions, getters } = options

  // 从当前 pinia 实例中以 id 获取初始状态(可能为空)
  const initialState = pinia.state.value[id]

  let store

  function setup() {
    if (!initialState && (!__DEV__ || !hot)) {
      /* istanbul ignore if */
      if (isVue2) {
        set(pinia.state.value, id, state ? state() : {})
      } else {
        pinia.state.value[id] = state ? state() : {}
      }
    }

    // 避免在pinia.state.value中创建状态
    const localState =
      // 如果是开发环境并且启用了热重载
      __DEV__ && hot
        ? // use ref() to unwrap refs inside state
          // 利用 ref() 内部自动解包(unwrap, 如果是 ref 则跳过, 不是则转换)的机制来绑定响应式
          // TODO: check if this is still necessary
          // TODO: 也许这个判断不是必要的?
          toRefs(ref(state ? state() : {}).value)
        : toRefs(pinia.state.value[id]) // 如果没有热重载则直接进行转换

    // 仅做初始状态的处理并转换为 setup 函数, 最终再调用 createSetupStore 函数创建最终的 store
    return assign(
      localState,
      actions,
      Object.keys(getters || {}).reduce((computedGetters, name) => {
        // 如果 getter 函数和 state 的键值同名则抛出警告
        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)
            // 获取之前创建的 store
            const store = pinia._s.get(id)!

            // 允许交叉使用商店
            /* istanbul ignore if */
            if (isVue2 && !store._r) return

            // 改变内部 getter this 指向, 从而指向插件本身
            return getters![name].call(store, store)
          })
        )
        return computedGetters
      }, {})
    )
  }

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

  return store
}

createSetupStore

createSetupStore 就是这次的重头戏了, 所有状态创建的核心逻辑都在这里面

export enum MutationType {
  direct = 'direct',
  patchObject = 'patch object',
  patchFunction = 'patch function',
}

// 工具函数, 用于判断是否是一个普通对象, 而非 function/array/map/set 这种值
export function isPlainObject(o) {
  return (
    o &&
    typeof o === 'object' &&
    Object.prototype.toString.call(o) === '[object Object]' &&
    typeof o.toJSON !== 'function'
  )
}

// 增加一个订阅函数
export function addSubscription(
  subscriptions,
  callback,
  detached,
  onCleanup
) {
  subscriptions.push(callback)

  const removeSubscription = () => {
    const idx = subscriptions.indexOf(callback)
    if (idx > -1) {
      subscriptions.splice(idx, 1)
      onCleanup()
    }
  }

  if (!detached && getCurrentScope()) {
    onScopeDispose(removeSubscription)
  }

  return removeSubscription
}

// 触发所有订阅
export function triggerSubscriptions(
  subscriptions,
  ...args
) {
  // 拷贝一份 subscriptions, 防止在执行回调时 subscriptions 发生变化
  subscriptions.slice().forEach((callback) => {
    callback(...args)
  })
}

// 工具函数, 合并响应式对象状态
function mergeReactiveObjects(target, patchToApply) {
  // 更新 Map 实例对象
  if (target instanceof Map && patchToApply instanceof Map) {
    patchToApply.forEach((value, key) => target.set(key, value))
  }
  // 更新 Set 实例对象
  if (target instanceof Set && patchToApply instanceof Set) {
    patchToApply.forEach(target.add, target)
  }

  // 无需遍历 Symbol,因为它们无论如何都无法序列化
  for (const key in patchToApply) {
    // 如果是原型属性, 则跳过
    if (!patchToApply.hasOwnProperty(key)) continue
    // 新的值
    const subPatch = patchToApply[key]
    // 旧的值
    const targetValue = target[key]

    // 如果是对象类型的值且它本身不是响应式对象, 则递归调用 mergeReactiveObjects
    if (
      isPlainObject(targetValue) &&
      isPlainObject(subPatch) &&
      target.hasOwnProperty(key) &&
      // isRef/isReactive 是 vue 内部用于判断是否是响应式对象的方法
      !isRef(subPatch) &&
      !isReactive(subPatch)
    ) {
      // NOTE: 在这里,我想警告不一致的类型,但这是不可能的,因为在设置存储中,人们可能会将属性的值启动为某种类型,例如 一个 Map,然后出于某种原因,在 SSR 期间,将其更改为 "undefined"。 当尝试水合时,我们想用 "undefined" 覆盖 Map。
      target[key] = mergeReactiveObjects(targetValue, subPatch)
    } else {
      // 否则直接赋值覆盖
      target[key] = subPatch
    }
  }

  return target
}

function createSetupStore(
  $id,
  setup,
  options,
  pinia,
  hot,
  isOptionsStore
) {
  // effectScope
  let scope

  const optionsForPlugin = assign(
    { actions: {} },
    // 对象型的整个 store,或者是函数型 store 的第三个参数
    options
  )

  /* istanbul ignore if */
  // 开发环境如果 effectScope 的 active 为 false 则表示 pinia 被提前销毁了
  if (__DEV__ && !pinia._e.active) {
    throw new Error('Pinia destroyed')
  }

  // watcher options for $subscribe
  // $subscribe 功能的实现其实就是添加了一个 watch 函数来监听变量的变化, 这个对象就是 watch 的第三个参数
  const $subscribeOptions = {
    deep: true,
    // flush: 'post',
  }

  if (__DEV__ && !isVue2) {
    // vue3 watch 支持 onTrigger 参数, 这个参数可以让底层框架做一些其他的事, 而不是将用户传入的参数包装一层再调用
    $subscribeOptions.onTrigger = (event) => {
      if (isListening) {
        debuggerEvents = event
        // 当 store 正在创建中并且 pinia 正在更新中则不触发这个事件
      } else if (isListening == false && !store._hotUpdating) {
        // 收集所有事件然后一起发送
        if (Array.isArray(debuggerEvents)) {
          debuggerEvents.push(event)
        } else {
          console.error(
            '🍍 debuggerEvents should be an array. This is most likely an internal Pinia bug.'
          )
        }
      }
    }
  }

  // internal state
  let isListening
  let isSyncListening
  let subscriptions = []
  let actionSubscriptions = []
  let debuggerEvents
  const initialState = pinia.state.value[$id]

  // 如果是 setup store 则不需要初始化 state
  if (!isOptionsStore && !initialState && (!__DEV__ || !hot)) {
    /* istanbul ignore if */
    if (isVue2) {
      set(pinia.state.value, $id, {})
    } else {
      pinia.state.value[$id] = {}
    }
  }

  const hotState = ref({})

  // 当前触发的 listener id
  // 避免同一时间触发太多 listener
  // https://github.com/vuejs/pinia/issues/1129
  let activeListener

  // $patch 函数的主要是用于覆盖现有store的状态, 在需要批量更新store的时候有奇效
  function $patch(
    // 用来覆盖 store 数据的状态或者是一个函数
    partialStateOrMutator
  ) {
    // 用于保存 patch 触发时的类型与值
    let subscriptionMutation
    isListening = isSyncListening = false

    // 由于 $patch 是同步的,所以每次触发的时候重置 debuggerEvents
    /* istanbul ignore else */
    if (__DEV__) {
      debuggerEvents = []
    }
    // 如果传入的
    if (typeof partialStateOrMutator === 'function') {
      // 如果是函数则执行函数, 并把 store 作为参数传入
      partialStateOrMutator(pinia.state.value[$id])
      // 设置更新值得 mutation 参数, 用于告诉通知 subscriptions 时的变更类型
      subscriptionMutation = {
        type: MutationType.patchFunction,
        storeId: $id,
        events: debuggerEvents,
      }
    } else {
      // 如果是对象则合并对象
      mergeReactiveObjects(pinia.state.value[$id], partialStateOrMutator)
      subscriptionMutation = {
        type: MutationType.patchObject,
        payload: partialStateOrMutator,
        storeId: $id,
        events: debuggerEvents,
      }
    }
    // 用于标识当前监听函数的id
    const myListenerId = (activeListener = Symbol())
    nextTick().then(() => {
      // 如果当前监听函数不是当前的监听函数则不触发
      if (activeListener === myListenerId) {
        isListening = true
      }
    })
    isSyncListening = true
    // 因为我们暂停了观察者,所以我们需要手动调用订阅
    triggerSubscriptions(
      subscriptions,
      subscriptionMutation,
      pinia.state.value[$id]
    )
  }

  // 如果是 options store 则可以调用 $reset 方法还原成初始值
  const $reset = isOptionsStore
    ? function $reset() {
        const { state } = options
        const newState = state ? state() : {}
        // 因为这个 $reset 最终会被放到一个单独的对象上,所以 this.$patch 其实就是上面的 $patch 函数,只是内部覆盖值的操作被简化成了直接覆盖整个对象
        this.$patch(($state) => {
          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)
  }

  /**
   * 包装 action 以处理订阅, 在值发生变化以后触发订阅
   *
   * @param name - actions 里的 key
   * @param action - actions 里的 value
   * @returns 返回一个包装了触发订阅参数的 action
   */
  function wrapAction(name, action) {
    return function () {
      // 在每次操作前更新一次 pinia,确保实例的正确
      setActivePinia(pinia)
      const args = Array.from(arguments)

      // 函数执行后的订阅函数(正常的订阅函数)
      const afterCallbackList = []
      // 函数触发错误后的订阅函数
      const onErrorCallbackList = []
      function after(callback) {
        afterCallbackList.push(callback)
      }
      function onError(callback) {
        onErrorCallbackList.push(callback)
      }

      // 触发所有添加的订阅函数
      triggerSubscriptions(actionSubscriptions, {
        args,
        name,
        store,
        after,
        onError,
      })

      // 用户获取判断 action 是否是异步的(返回一个 promise)
      let ret
      try {
        // 修改 this 指向为 store
        ret = action.apply(this && this.$id === $id ? this : store, args)
      } catch (error) {
        // 如果 action 执行出错, 触发 onError 订阅函数
        triggerSubscriptions(onErrorCallbackList, error)
        throw error
      }

      // 如果 action 是异步的, 触发 after 订阅函数
      if (ret instanceof Promise) {
        return ret
          .then((value) => {
            triggerSubscriptions(afterCallbackList, value)
            return value
          })
          .catch((error) => {
            triggerSubscriptions(onErrorCallbackList, error)
            return Promise.reject(error)
          })
      }

      // 如果是同步的函数则在执行完成后触发 after 订阅函数
      triggerSubscriptions(afterCallbackList, ret)
      return ret
    }
  }

  // 用于 devtools 的 HMR
  const _hmrPayload = markRaw({
    actions: {},
    getters: {},
    state: [],
    hotState,
  })

  const partialStore = {
    _p: pinia,
    // _s: scope,
    $id,
    $onAction: addSubscription.bind(null, actionSubscriptions),
    $patch,
    $reset,
    $subscribe(callback, options = {}) {
      const removeSubscription = addSubscription(
        subscriptions,
        callback,
        options.detached,

        // 清理函数, 用于取消 scopeEffect 收集的副作用
        () => stopWatcher()
      )
      const stopWatcher = scope.run(() =>
        watch(
          () => pinia.state.value[$id],
          (state) => {
            if (options.flush === 'sync' ? isSyncListening : isListening) {
              callback(
                {
                  storeId: $id,
                  type: MutationType.direct,
                  events: debuggerEvents as DebuggerEvent,
                },
                state
              )
            }
          },
          assign({}, $subscribeOptions, options)
        )
      )!

      // 返回一个清理函数, 常用的 API 设计风格了
      return removeSubscription
    },
    $dispose,
  }

  /* istanbul ignore if */
  if (isVue2) {
    // start as non ready
    partialStore._r = false
  }

  const store = reactive(
    __DEV__ || (__USE_DEVTOOLS__ && IS_CLIENT)
      // 如果是开发环境且开启了 devtools, 则添加 hmr 对象同时将 store 作为响应式对象
      ? assign(
          {
            _hmrPayload,
            _customProperties: markRaw(new Set()), // devtools custom properties
          },
          partialStore
        )
      // 否则直接将 store 作为响应式对象
      : partialStore
  )

  // 现在存储部分存储,以便存储的设置可以在完成之前相互实例化,而不会创建无限循环。
  pinia._s.set($id, store)

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

  // TODO: idea 创建 skipSerialize 将属性标记为不可序列化并跳过它们
  const setupStore = runWithContext(() =>
    pinia._e.run(() => (scope = effectScope()).run(setup)!)
  )!

  // 覆盖现有操作以支持 $onAction
  for (const key in setupStore) {
    const prop = setupStore[key]

    // 必须得是 ref/reactive 同时不能是 computed
    if ((isRef(prop) && !isComputed(prop)) || isReactive(prop)) {
      // 将其标记为要序列化的状态
      if (__DEV__ && hot) {
        set(hotState.value, key, toRef(setupStore as any, key))
      } else if (!isOptionsStore) { // createOptionStore 直接在 pinia.state.value 中设置状态,因此可以跳过, 只需要处理普通的对象 store
        // 在设置存储中,我们必须对状态进行水合,并将pinia状态树与用户刚刚创建的 refs 同步
        if (initialState && shouldHydrate(prop)) {
          if (isRef(prop)) {
            prop.value = initialState[key]
          } else {
            // 可能是一个反应对象, 递归合并
            // @ts-expect-error: prop is unknown
            mergeReactiveObjects(prop, initialState[key])
          }
        }
        // 将 ref 转移到 pinia 内部状态以保持一切同步处理
        /* istanbul ignore if */
        if (isVue2) {
          set(pinia.state.value[$id], key, prop)
        } else {
          pinia.state.value[$id][key] = prop
        }
      }

      /* istanbul ignore else */
      if (__DEV__) {
        // 把这个key放进热更新状态列表里
        _hmrPayload.state.push(key)
      }
      // action
    } else if (typeof prop === 'function') {
      // 这是一个热模块替换 store,因为 hotUpdate 方法需要在正确的上下文中执行此操作
      // 所以如果是开发环境并且启用了热更新则不要包装它
      const actionValue = __DEV__ && hot ? prop : wrapAction(key, prop)

      if (isVue2) {
        set(setupStore, key, actionValue)
      } else {
        setupStore[key] = actionValue
      }

      if (__DEV__) {
        // 更新热更新的值
        _hmrPayload.actions[key] = prop
      }

      // 列出 actions,以便可以在插件中使用它们
      optionsForPlugin.actions[key] = prop
    } else if (__DEV__) {
      // 添加对 devtools 的支持
      if (isComputed(prop)) {
        _hmrPayload.getters[key] = isOptionsStore
          ? options.getters[key]
          : prop
        if (IS_CLIENT) {
          const getters = (setupStore._getters) || ((setupStore._getters = markRaw([])))
          getters.push(key)
        }
      }
    }
  }

  // 添加 state、getters 和 actions 属性
  /* istanbul ignore if */
  if (isVue2) {
    Object.keys(setupStore).forEach((key) => {
      set(store, key, setupStore[key])
    })
  } else {
    assign(store, setupStore)
    // 允许使用“storeToRefs()”检索反应对象。 必须在分配给反应对象后调用。 让“storeToRefs()”与“reactive()”一起使用 #799
    assign(toRaw(store), setupStore)
  }

  // 使用它而不是使用 setter 计算,以便能够在任何地方创建它,而无需将计算的生命周期链接到首次创建存储的位置。
  Object.defineProperty(store, '$state', {
    get: () => (__DEV__ && hot ? hotState.value : pinia.state.value[$id]),
    set: (state) => {
      if (__DEV__ && hot) {
        throw new Error('cannot set hotState')
      }
      $patch(($state) => {
        assign($state, state)
      })
    },
  })

  // 在插件之前添加 hotUpdate 以允许它们覆盖它
  if (__DEV__) {
    // 执行热更新时触发的操作
    store._hotUpdate = markRaw((newStore) => {
      store._hotUpdating = true
      newStore._hmrPayload.state.forEach((stateKey) => {
        if (stateKey in store.$state) {
          const newStateTarget = newStore.$state[stateKey]
          const oldStateSource = store.$state[stateKey]
          if (
            typeof newStateTarget === 'object' &&
            isPlainObject(newStateTarget) &&
            isPlainObject(oldStateSource)
          ) {
            patchObject(newStateTarget, oldStateSource)
          } else {
            // 转移 ref
            newStore.$state[stateKey] = oldStateSource
          }
        }
        // 修补直接访问属性以允许 store.stateProperty 用作 store.$state.stateProperty
        set(store, stateKey, toRef(newStore.$state, stateKey))
      })

      // 删除已删除的状态属性
      Object.keys(store.$state).forEach((stateKey) => {
        if (!(stateKey in newStore.$state)) {
          del(store, stateKey)
        }
      })

      // 避免开发工具将其记录为 mutation
      isListening = false
      isSyncListening = false
      pinia.state.value[$id] = toRef(newStore._hmrPayload, 'hotState')
      isSyncListening = true
      nextTick().then(() => {
        isListening = true
      })

      // 包装 actions
      for (const actionName in newStore._hmrPayload.actions) {
        const action = newStore[actionName]

        set(store, actionName, wrapAction(actionName, action))
      }

      // TODO: 不确定这在 setup store 和 option store 中是否都有效
      for (const getterName in newStore._hmrPayload.getters) {
        const getter = newStore._hmrPayload.getters[getterName]
        const getterValue = isOptionsStore
          ? // option store 中的 getters 的特殊处理
            computed(() => {
              setActivePinia(pinia)
              return getter.call(store, store)
            })
          : getter

        set(store, getterName, getterValue)
      }

      // 删除已删除的 getters 属性
      Object.keys(store._hmrPayload.getters).forEach((key) => {
        if (!(key in newStore._hmrPayload.getters)) {
          del(store, key)
        }
      })

      // 删除已删除的 actions 属性
      Object.keys(store._hmrPayload.actions).forEach((key) => {
        if (!(key in newStore._hmrPayload.actions)) {
          del(store, key)
        }
      })

      // 更新 devtools 中使用的值并允许稍后删除新属性
      store._hmrPayload = newStore._hmrPayload
      store._getters = newStore._getters
      store._hotUpdating = false
    })
  }

  if (__USE_DEVTOOLS__ && IS_CLIENT) {
    const nonEnumerable = {
      writable: true,
      configurable: true,
      // 避免在开发工具尝试显示此属性时发出警告
      enumerable: false,
    }

    // 避免在开发工具中列出内部属性
    ;(['_p', '_hmrPayload', '_getters', '_customProperties'] as const).forEach(
      (p) => {
        Object.defineProperty(
          store,
          p,
          assign({ value: store[p] }, nonEnumerable)
        )
      }
    )
  }

  if (isVue2) {
    // 在插件之前将 store 标记为就绪
    store._r = true
  }

  // 应用所有的插件
  pinia._p.forEach((extender) => {
    if (__USE_DEVTOOLS__ && IS_CLIENT) {
      // 插件内部可能会为 store 添加新的属性, 这些属性可能是响应式的, 所以要将他们收集起来
      const extensions = scope.run(() =>
        extender({
          store: store,
          app: pinia._a,
          pinia,
          options: optionsForPlugin,
        })
      )!
      Object.keys(extensions || {}).forEach((key) =>
        // 把新增的属性添加到 store._customProperties 中, 以便在 devtools 中显示
        store._customProperties.add(key)
      )
      assign(store, extensions)
    } else {
      // 执行同样的操作, 将插件返回的值和 store 合并
      assign(
        store,
        scope.run(() =>
          extender({
            store: store,
            app: pinia._a,
            pinia,
            options: optionsForPlugin,
          })
        )!
      )
    }
  })

  // 对传入的 state 进行格式判断, 不允许是一个自定义类或内置类, 只能是传统对象, 同样只在开发器间做判断
  if (
    __DEV__ &&
    store.$state &&
    typeof store.$state === 'object' &&
    typeof store.$state.constructor === 'function' &&
    !store.$state.constructor.toString().includes('[native code]')
  ) {
    console.warn(
      `[🍍]: The "state" must be a plain object. It cannot be\n` +
        `\tstate: () => new MyClass()\n` +
        `Found in store "${store.$id}".`
    )
  }

  // 只把初始状态下的 pinia store 用作 SSR 时的初始状态
  if (
    initialState &&
    isOptionsStore &&
    // 可以自定义 hydrate 方法, 用于在 SSR 时, 将初始状态同步到 store 中
    options.hydrate
  ) {
    options.hydrate(
      store.$state,
      initialState
    )
  }

  // 表示 store 已经被创建, 内部状态正在监听
  isListening = true
  isSyncListening = true
  return store
}

到这里函数就结束了, 通篇看下来它在开发期间做了很多类型处理与判断, 这也使得在使用pinia进行开发的时候就能发现一些问题, 避免在无意间埋下一些意料之外的隐患. 同时针对开发器间的 HMR 也是做了相当多的处理, 使得在开发期间的体验更加的友好, 所以如果想要自己开发一个 vue 库的话, 也可以参考一下 pinia 的实现, 从中学习到一些开发技巧, 比如如何和 vue-devtools 更好集成, 如何使用 HMR 替换等等.

当然, 除了实现的代码优雅之外, Pinia 内部做的 ts 类型提示也是很完善的, 有空也可以学习一下(挖坑).

学到了什么

  1. defineStore 内部做了些什么, 如果创建了一个 pinia store 实例
  2. pinia 的插件在 pinia 实例注册前使用和注册后使用的区别
  3. 传入函数来创建一个 store 会比使用对象形式来创建 store 更快, 因为对象内部会将对象转换为 setup store, 然后再创建
  4. $subscribe() 方法其实是通过 Vue 内部的 watch 来监听状态的变化来实现的
  5. 监听状态变化的回调会有哪些参数
  6. 如何编写一个兼容Vue2/Vue3的库(使用 vue-demi 对当前应用的环境进行判断)
  7. pinia 的插件能拿到哪些store的上下文及参数(store, app, pinia, options)

以上.


CD ..