Pinia源码解析【2.1.4】

278 阅读10分钟

参考文档

pinia当前版本: 2.1.4

pinia是 Vue 的专属状态管理库,并且可以同时支持 Vue 2 和 Vue 3。

在Vue3的项目中,我们都会优先使用Pinia,所以了解其基本的底层原理,有助于我们在项目中更好的应用。

1.createPinia

在Vue3项目中使用pinia时,都会从pinia中引入一个createPinia方法,然后使用这个方法创建一个pinia实例。

import { createPinia } from 'pinia'
// 创建pinia
const pinia = createPinia()

接下来我们就从import { createPinia } from 'pinia'这句代码开始pinia源码的学习。

首先查看createPinia的源码:

// packages/pinia/src/createPinia.ts

/**
 * Creates a Pinia instance to be used by the application
 */
export function createPinia(): Pinia {
  const scope = effectScope(true)
  // 初始化响应式state:默认为一个ref数据【目的是存储:所有store的state的集合】
  // RefImpl:{ value: proxy:{} }
  // NOTE: here we could check the window object for a state and directly set it
  // if there is anything like it with Vue 3 SSR
  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实例对象
  // 这里用markRaw方法标记了Pinia对象,不需要被转换为响应式数据
  const pinia: Pinia = markRaw({
    // 定义的Install方法:每个vue插件都需要显示定义,在app.use时调用插件的install方法
    install(app: App) {
      // this allows calling useStore() outside of a component setup after
      // installing pinia's plugin
      // 设置为当前活跃的pinia
      setActivePinia(pinia)
      // 在Vue3应用的情况下:将pinia实例对象挂载到app上
      if (!isVue2) {
        // 存储Vue3 app应用实例
        pinia._a = app
        // app.provide()提供一个值,可以在应用中的所有后代组件中注入使用【重点】
        app.provide(piniaSymbol, pinia)
        // 设置全局属性$pinia:在vue应用内的每个组件都可以通过this.$pinia访问
        app.config.globalProperties.$pinia = pinia
        // 注册开发者工具
        /* istanbul ignore else */
        if (USE_DEVTOOLS) {
          registerPiniaDevtools(app, pinia)
        }
        // 将插件列表添加到_p属性
        toBeInstalled.forEach((plugin) => _p.push(plugin))
        // 清空待安装插件列表
        toBeInstalled = []
      }
    },

    // pinia对外暴露的插件注册方法
    use(plugin) {
      if (!this._a && !isVue2) {
        // vue3: 添加到待安装插件列表:初始化时使用
        toBeInstalled.push(plugin)
      } else {
        // vue2:直接添加到_p属性
        _p.push(plugin)
      }
      return this
    },

    _p, // 插件列表
    // it's actually undefined here
    // @ts-expect-error
    _a: null,// vue3 app应用实例
    _e: scope, // effectScope
    _s: new Map<string, StoreGeneric>(),// 一个map结构,存储storeId,与store实例的映射关系
    state, // 所有的state的集合:每个store初始化时,都会将处理后state数据挂载到Pinia.state.value[$id]下面
  })

  // 注册开发者工具插件
  // 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 && typeof Proxy !== 'undefined') {
    pinia.use(devtoolsPlugin)
  }

  // 返回pinia实例对象
  return pinia
}

可以看出createPinia方法的内容比较简单,主要就是创建了一个Pinia实例并返回。pinia实例里面定义了两个方法和几个属性,

我们首先看两个方法:

  • install方法:将pinia注册到vue实例。
  • use方法:将其他插件注册到pinia实例。
const app = createApp(App)
const pinia = createPinia()

// 注册pinia:会在内部调用pinia的install方法
app.use(pinia)

install方法内容并不多,最主要就是将pinia实例注入到全局,让vue应用下的每个组件实例都可以使用。

app.provide(piniaSymbol, pinia)

然后重点介绍两个属性:

  • pinia._s:可以看出这个属性是一个map结构,它的作用就是存储我们在项目中定义的store实例,它是以键值对的方式进行注册pinia._s.set($id, store),它的作用是在我们多次引用useStore时,不会重复新建Store,而是直接返回已存在的对象。

  • pinia.state:这个属性被初始化为一个ref数据,它的作用是存储每个store实例中的state数据,每个store在创建时,都会将state进行处理后挂载到Pinia.state.value[$id]下面,具体的行为可以在后面createXXXStore中查看。

2.defineStore

下面我们开始阅读defineStore的源码:

// packages/pinia/src/store.ts

/**
 *  两个参数:
 *  参数1,store的唯一标识符, 传值为string或者{id:string}
 *  参数2,可接收两类值,Setup函数或者Options对象;【相当于使用组合式和选项式,随便用哪种都行】
 *  参数3,可选,Options对象, 基本没有用到过
 */
export function defineStore(
  // TODO: add proper types from above
  idOrOptions: any,
  setup?: any,
  setupOptions?: any
): StoreDefinition {
  // 声明两个变量
  let id: string
  let options:
    | DefineStoreOptions<
        string,
        StateTree,
        _GettersTree<StateTree>,
        _ActionsTree
      >
    | DefineSetupStoreOptions<
        string,
        StateTree,
        _GettersTree<StateTree>,
        _ActionsTree
      >

  // 根据第二参数的类型来判断是否使用的setupStore模式
  const isSetupStore = typeof setup === 'function'
  if (typeof idOrOptions === 'string') {
    id = idOrOptions
    // the option store setup will contain the actual options in this case
    // 这里如果为setupStore,setupOptions就是undefined,即options为undefined
    options = isSetupStore ? setupOptions : setup
  } else {
    options = idOrOptions
    id = idOrOptions.id

    if (__DEV__ && typeof id !== 'string') {
      throw new Error(
        `[🍍]: "defineStore()" must be passed a store id as its first argument.`
      )
    }
  }

  function useStore(pinia?: Pinia | null, hot?: StoreGeneric): StoreGeneric {
    // ...
  }

  useStore.$id = id

  return useStore
}

defineStore方法比较重要,是我们在定义Store时要经常使用的,defineStore方法只有两个参数:

  • 参数1:store实例的id,必须保证独一无二。
  • 参数2:可接收两类值,Setup函数或者Options对象

这里defineStore方法里面大部分代码都是useStore函数的内容,所以我们先把它折叠起来,后面再分析。

注意:我们在阅读开源项目源码的时候,一定要学会内容拆分,因为有的函数源码量非常大,有的能达到几百行甚至上千行【比如Vue3源码baseCreateRenderer基础渲染器函数有两千多行代码】,我们要拆分其内容,降低复杂度,然后才能更好的解析其主要逻辑。在熟悉其主要逻辑之后,我们可以进行源码调试,通过step步入每个函数内容,验证之前的逻辑分析。

当我们把useStore函数折叠后,defineStore方法的内容就很简洁明了了,首先处理传入的参数,然后定义了一个useStore方法并且返回,这里的重点是定义的useStore方法:

// 在项目中
const useMainStore = defineStore('main', {...})

这里我们在项目中声明的useMainStore函数内容:就是调用defineStore后返回的useStore方法。

下面我们继续解析useStore源码:

useStore

// store方法
  function useStore(pinia?: Pinia | null, hot?: StoreGeneric): StoreGeneric {
    // 是否在子组件下使用
    const hasContext = hasInjectionContext()
    // 获取当前的pinia实例, 如果是开发模式下(activePinia._testing), 则使用inject获取pinia实例, 否则考虑直接使用传入的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)
    if (pinia) setActivePinia(pinia)

    if (__DEV__ && !activePinia) {
      throw new Error(
        `[🍍]: "getActivePinia()" was called but there was no active Pinia. Did you forget to install pinia?\n` +
          `\tconst pinia = createPinia()\n` +
          `\tapp.use(pinia)\n` +
          `This will fail in production.`
      )
    }

    pinia = activePinia!

    // pinia的_s属性为一个map结构,存储键值对:键为storeId,值为store实例
    // 从map结构中查询该storeId,如果不存在,则执行创建逻辑
    if (!pinia._s.has(id)) {
      // creating the store registers it in `pinia._s`
      // 根据之前的isSetupStore变量值,划分为两种模式创建逻辑
      if (isSetupStore) {
        createSetupStore(id, setup, options, pinia)
      } else {
        createOptionsStore(id, options as any, pinia)
      }

      /* istanbul ignore else */
      if (__DEV__) {
        // @ts-expect-error: not the right inferred type
        useStore._pinia = pinia
      }
    }

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

    // 处理非ssr模式下, 将store实例存储到vm中, 供devTools使用
    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
      }
    }

    // StoreGeneric cannot be casted towards Store
    return store as any
  }

useStore方法一进来调用了inject()来获取当前的pinia实例,这里拿到pinia实例是为了把即将创建的Store实例注册到Pinia._s属性中,以及将state中的数据都挂载到Pinia.state.value[$id]下面。Pinia._s属性是一个map结构,在最前面的createPinia方法有展示过【它的key是storeID,它的值是store实例】。

注意: 这里能够通过inject(piniaSymbol, null)获取到pinia,是因为在Install方法里面app.provide()已经提供了。同时我们应该知道每一个Symbol()都是唯一的,即Symbol() !== Symbol(),所以我们可以根据piniaSymbol可以获取到正确的pinia。

这里根据storeId来查询Pinia._s属性值是否存在对应的store实例:

  • 存在:则取出并直接返回store实例。
  • 不存在:则根据之前isSetupStore变量的值来决定创建store实例的逻辑。

综上所述: useStore方法主要作用就是返回Store实例供我们使用【无则创建,有则直接返回,避免重复创建】。

下面我们开始分析创建store的过程。

有两种定义Store的模式,对应着两种创建Store的方法:

// 1,OptionsStore模式
export const useCounterStore = defineStore('counter', {
  state: () => {
    return { count: 0 }
  },
  actions: {
    increment() {
      this.count++
    },
  },
})
// 2,SetupStore模式
export const useCounterStore = defineStore('counter', () => {
  const count = ref(0)
  function increment() {
    count.value++
  }
  return { count, increment }
})

createOptionsStore

我们首先查看createOptionsStore源码:

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> {
  // 从options对象中取出选项参数
  const { state, actions, getters } = options
  // 获取初始state:根据storeId获取Store中的初始state数据
  const initialState: StateTree | undefined = pinia.state.value[id]
  // 创建store变量
  let store: Store<Id, S, G, A>

  // 定义setup方法【重要】
  function setup() {
    // 如果不存在初始化数据,则执行state()
    if (!initialState && (!__DEV__ || !hot)) {
      /* istanbul ignore if */
      if (isVue2) {
        set(pinia.state.value, id, state ? state() : {})
      } else {
        pinia.state.value[id] = state ? state() : {}
      }
    }

    // avoid creating a state in pinia.state.value
    // 重点,转换成ref数据
    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])

    // Object.assign() 合并成setupStore原始对象
    return assign(
      localState,
      actions,
      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}".`
          )
        }

        // getter, 这里markRaw加上computed组合怎么运行? 需要看对应源码
        computedGetters[name] = markRaw(
          computed(() => {
            setActivePinia(pinia)
            // it was created just before
            const store = pinia._s.get(id)!

            // allow cross using stores
            /* istanbul ignore next */
            if (isVue2 && !store._r) return

            // @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从options选项对象中取出所需要的数据,然后定义了一个setup函数来处理相关的数据【初始化Store的方法】, 在setup函数中会将store设置到pinia.state.value[id]中,最后调用了createSetupStore这个方法来创建了一个Store实例,并且返回这个实例。

综上所述: 当我们使用OptionsStore模式来定义Store时,createOptionsStore方法内部会将我们传入数据包装成一个setup函数,最终内部还是调用了createSetupStore这个方法来创建的Store实例,即将OptionsStore转换成了SetupStore类型,所以createSetupStore函数才是Pinia中创建Store的真正核心,下面我们继续深入createSetupStore方法源码。

createSetupStore

createSetupStore方法的源码非常多,有五百多行,这里我们就不展示完整代码了,只贴一些关键的逻辑代码:

// 创建SetupStore
function createSetupStore<
  Id extends string,
  SS extends Record<any, unknown>,
  S extends StateTree,
  G extends Record<string, _Method>,
  A extends _ActionsTree
>(
  $id: Id,
  setup: () => SS,
  options:
    | DefineSetupStoreOptions<Id, S, G, A>
    | DefineStoreOptions<Id, S, G, A> = {},
  pinia: Pinia,
  hot?: boolean,
  isOptionsStore?: boolean
): Store<Id, S, G, A> {
    ...
    // 获取初始state:根据storeId获取Store中的初始state数据
    const initialState = pinia.state.value[$id] as UnwrapRef<S> | undefined
    // 专门处理setupStore模式
    if (!isOptionsStore && !initialState && (!__DEV__ || !hot)) {
      if (isVue2) {
        set(pinia.state.value, $id, {})
      } else {
        // 初始化为一个空对象
        pinia.state.value[$id] = {}
      }
    }
    ...
    // 定义了一些方法
    // 在partialStore中使用$patch方法
    function $patch() {}
    // 非setup模式下, 定义$reset方法, 调用$reset()方法可以将 state 重置为初始值, 在plugions中会使用
    function $reset() {} // 调用$reset()方法可以将 state 重置为初始值
    // 在partialStore中定义$dispose方法, 停止并且删除所有的订阅, 在plugins使用
    function $dispose() {}
    // 包裹一个action方法, 用来触发错误或者完成回调
    function wrapAction() {}
    ...
    // 定义了一个基础store:并且在它的实例上挂载了一些方法, 这里的所有函数都是用于plugins中使用
    const partialStore = {
      _p: pinia,
      // _s: scope,
      $id,
      $onAction: addSubscription.bind(null, actionSubscriptions),
      $patch, // 修改state
      $reset, // 重置state
      $subscribe() {}, // 监听
      $dispose,
    } as _StoreWithState<Id, S, G, A>
    ...
    // 使用partialStore为原对象: 创建一个响应式的Store实例
    const store = reactive(partialStore)
    
    // 将store添加到pinia._s的map结构中【重点】
    pinia._s.set($id, store)
    
    const runWithContext =
    (pinia._a && pinia._a.runWithContext) || fallbackRunWithContext

    // TODO: idea create skipSerialize that marks properties as non serializable and they are skipped
    // 在单独一个scope中执行setup方法, 并获取返回值, 生成原始setupStore对象
    const setupStore = pinia._e.run(() => {
      scope = effectScope()
      return runWithContext(() => scope.run(setup))
    })!
    
    // 遍历 setupStore 属性,对其进行处理转换【重点是针对setup模式下的state数据挂载】
    // options模式会在setup函数中挂载到state中
  for (const key in setupStore) {
    const prop = setupStore[key]

    // 如果是响应式数据并且不是计算属性, 此时需要手动将initialState数据赋值给响应式数据
    if ((isRef(prop) && !isComputed(prop)) || isReactive(prop)) {
      // mark it as a piece of state to be serialized
      if (!isOptionsStore) {
        // in setup stores we must hydrate the state and sync pinia state tree with the refs the user just created
        // 如果存在初始state数据,则将初始state数据赋值给响应式数据(通常在热更新时存在)
        if (initialState && shouldHydrate(prop)) {
          if (isRef(prop)) {
            prop.value = initialState[key]
          } else {
            // probably a reactive object, lets recursively assign
            // @ts-expect-error: prop is unknown
            mergeReactiveObjects(prop, initialState[key])
          }
        }
        // transfer the ref to the pinia state to keep everything in sync
        /* istanbul ignore if */
        if (isVue2) {
          set(pinia.state.value[$id], key, prop)
        } else {
          // 赋值到state中, options模式会在setup函数中挂载到state中
          pinia.state.value[$id][key] = prop
        }
      }
      // action
    } else if (typeof prop === 'function') {
      const actionValue = __DEV__ && hot ? prop : wrapAction(key, prop)
      // this a hot module replacement store because the hotUpdate method needs
      // to do it with the right context
      /* istanbul ignore if */
      if (isVue2) {
        set(setupStore, key, actionValue)
      } else {
        // 更新为wrapAction
        setupStore[key] = actionValue
      }

      // list actions so they can be used in plugins
      optionsForPlugin.actions[key] = prop
    } else if (__DEV__) {
      // 如果是dev模式下加到devtools中
      // add getters for devtools
      if (isComputed(prop)) {
        _hmrPayload.getters[key] = isOptionsStore
          ? // @ts-expect-error
            options.getters[key]
          : prop
        if (IS_CLIENT) {
          const getters: string[] =
            (setupStore._getters as string[]) ||
            // @ts-expect-error: same
            ((setupStore._getters = markRaw([])) as string[])
          getters.push(key)
        }
      }
    }
  }

  // 将setupStore中的属性添加到store中
  // add the state, getters, and action properties
  /* istanbul ignore if */
  if (isVue2) {
    Object.keys(setupStore).forEach((key) => {
      set(store, key, setupStore[key])
    })
  } else {
    assign(store, setupStore)
    // allows retrieving reactive objects with `storeToRefs()`. Must be called after assigning to the reactive object.
    // Make `storeToRefs()` work with `reactive()` #799
    assign(toRaw(store), setupStore)
  }
​
  // 定义$state访问器属性,可以通过 store.$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) => {
        assign($state, state)
      })
    },
  })
    
    # 返回store实例对象
    return store
}

createSetupStore方法里面的内容很多,下面我们贴上一些重点的逻辑逐个解析。

a.挂载初始state数据对象:

// 获取初始state:根据storeId获取Store中的初始state数据
const initialState = pinia.state.value[$id] | undefined
# 专门处理setupStore模式
if (!isOptionsStore && !initialState && (!__DEV__ || !hot)) {
  if (isVue2) {
    set(pinia.state.value, $id, {})
  } else {
    // 初始化为一个空对象
	pinia.state.value[$id] = {}
  }
}

其实上面这段逻辑在createOptionsStore方法中也存在,而这里是专门处理setupStore模式定义store用的,所以这里新增了一个变量isOptionsStore来做区分。

这段代码主要的作用就是pinia.state.value属性下挂载一个空的state数据对象,key为传入的StoreID。

setupStore模式下:state直接被初始化为一个空对象,因为刚开始它并不知道state会有什么内容,需要经过setup()调用,初始化里面的ref/reactive等响应式数据,处理之后才会挂载到这个空对象里面。

OptionsStore模式下:直接调用state函数,即可得到初始的state数据对象。

# 调用state()
pinia.state.value[id] = state ? state() : {}

b.$reset方法在option模式下重置state对象

# OptionsStore模式下,重写了$reset方法,
const $reset = isOptionsStore ? function $reset(){} : noop

OptionsStore模式下:我们可以通过store.$reset()重置state数据对象。

而$reset方法的内容其实也很简单:

# $reset方法
function $reset(this: _StoreWithState<Id, S, G, A>) {
  // 取出state方法
  const { state } = options as DefineStoreOptions<Id, S, G, A>
  # 调用state方法:获取初始的state对象
  const newState = state ? state() : {}
  // 利用$patch方法修改state数据,
  this.$patch(($state) => {
    // Object.assign:用原始数据覆盖现在的数据
	assign($state, newState)
  })
}

setupStore模式下: 无法通过$reset重置, 因为这个模式下state的初始状态是通过调用setup函数得到的。如果我们调用$reset在开发模式会给出以下警告提示,而在生产模式下不会造成任何副作用。

Store "${$id}" is built using the setup syntax and does not implement $reset()

c.存储已经创建的store实例:

# 将store添加到pinia._s的map结构中【重点】
pinia._s.set($id, store)

在之前的useStore函数中,我们有通过pinia._s.has(id)来判断是否已存在目标Store,如果存在则直接返回Store对象。这里用一个map结构来存储已经创建好的Store实例,避免重复新建Store,因为我们可能会在多个组件中引用一个Store对象。

// 多次引用不会重复新建Store
const mainStore = useMainStore();

在Vue3的源码中也存在这样的逻辑处理,使用一个全局的map结构来存储项目中的target与Proxy对象。

d.对setupStore对象的处理:

  • 首先是原始store对象的生成:
// setupStore是最原始的store对象
  const runWithContext =
    (pinia._a && pinia._a.runWithContext) || fallbackRunWithContext

  // TODO: idea create skipSerialize that marks properties as non serializable and they are skipped
  // 在单独一个scope中执行setup方法, 并获取返回值, 生成原始setupStore对象
  const setupStore = pinia._e.run(() => {
    scope = effectScope()
    return runWithContext(() => scope.run(setup))
  })!

setupStore模式下setup函数内容比较简单:

export const useCounterStore = defineStore('counter', () => {
  const count = ref(0)
  function increment() {
    count.value++
  }
  
  // 暴露的数据和方法
  return { count, increment }
})

调用结果就是return返回的对象。

OptionsStore模式下setup内容比较复杂,需要额外的处理:

// OptionsStore的setup初始化过程
  // 定义setup方法【重要】
  function setup() {
    // 如果不存在初始化数据,则执行state()
    if (!initialState && (!__DEV__ || !hot)) {
      /* istanbul ignore if */
      if (isVue2) {
        set(pinia.state.value, id, state ? state() : {})
      } else {
        pinia.state.value[id] = state ? state() : {}
      }
    }

    // avoid creating a state in pinia.state.value
    // 重点,转换成ref数据, 如果是热更新则重新生成state, 否则使用pinia.state.value[id]
    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])

    // Object.assign() 合并成setupStore原始对象
    return assign(
      localState,
      actions,
      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}".`
          )
        }

        // getter, 并且使用markRaw的方式将生成的computed标记为不作为响应式对象使用
        // 使用storeToRefs后可以作为响应式数据
        computedGetters[name] = markRaw(
          computed(() => {
            setActivePinia(pinia)
            // it was created just before
            const store = pinia._s.get(id)!

            // allow cross using stores
            /* istanbul ignore next */
            if (isVue2 && !store._r) return

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

OptionsStore的setup方法内容比较多,主要就是为了处理state数据【将state中的内容全部转换为ref数据】和getters,最后使用对象的Object.assign()方法将state、actions、getters处理后的内容合并到一个新对象中,并且返回。

所以OptionsStore的setup方法调用后的返回值:就是我们需要的原始store对象即setupStore。

  • 循环处理原始setupStore对象中的数据。
  // 得到setupStore后, 遍历 setupStore 属性,对其进行处理转换【重点是针对setup模式下的state数据挂载】, 使用在store.$state中
  // options模式会在setup函数中挂载到state中
  for (const key in setupStore) {
    const prop = setupStore[key]

    // 如果是响应式数据并且不是计算属性, 此时需要手动将initialState数据赋值给响应式数据
    if ((isRef(prop) && !isComputed(prop)) || isReactive(prop)) {
      if (!isOptionsStore) {
        // in setup stores we must hydrate the state and sync pinia state tree with the refs the user just created
        // 如果存在初始state数据,则将初始state数据赋值给响应式数据(通常在热更新时存在)
        if (initialState && shouldHydrate(prop)) {
          if (isRef(prop)) {
            prop.value = initialState[key]
          } else {
            // probably a reactive object, lets recursively assign
            mergeReactiveObjects(prop, initialState[key])
          }
        }
        // transfer the ref to the pinia state to keep everything in sync
        if (isVue2) {
          set(pinia.state.value[$id], key, prop)
        } else {
          // 赋值到state中, options模式会在setup函数中挂载到state中
          pinia.state.value[$id][key] = prop
        }
      }

      // action
    } else if (typeof prop === 'function') {
      const actionValue = wrapAction(key, prop)
      // this a hot module replacement store because the hotUpdate method needs
      // to do it with the right context
      if (isVue2) {
        set(setupStore, key, actionValue)
      } else {
      // 更新为包裹后的action方法
        setupStore[key] = actionValue
      }
    }
  }
  • 最后挂载到store实例上:
# 将setupStore中的属性和方法挂载到新建的Store实例上
if (isVue2) {
  // vue2:通过set方法来新增响应式数据属性
  Object.keys(setupStore).forEach((key) => {
    set(store, key, setupStore[key])
  })
} else {
  // vue3:将处理后setupStore中的内容挂载到Store实例上【state数据和actions中的方法】
  assign(store, setupStore)
}

因为这里的store实例是前面新建的响应式数据对象,只存在最基础的属性和方法 【基础版Store】

const store = reactive(partialStore)

所以这里需要将我们真正的store中的内容 【即state数据和actions中的方法】 挂载到新建的这个Store实例上,这样这个Store实例才算真正的创建完成,包含了我们所需要的属性和方法。

(五)最后我们再看看 $state属性:

# 定义了一个访问器属性$state
Object.defineProperty(store, '$state', {
  // 访问代理
  get: () => pinia.state.value[$id],
  // 非直接修改的setter,而是在内部使用了$patch方法
  set: (state) => {
    $patch(($state) => {
      # 覆盖key,而非整个替换
      assign($state, state)
    })
  }
})
  • getter:可以通过store.$store访问state数据,getter内部设置了对应的访问代理。
  • setter:这里setter并不是直接去修改store的state,而是内部通过patch来安全的修改:

需要预防出现下面这样的操作:你不能完全替换掉 store 的 state,因为那样会破坏其响应性。

store.$state = { count: 24 }

所以setter内部是通过$patch方法来修改state的,这样即使出现上面这样的代码也不会破坏state的响应性。

3.总结

再总结一下两种模式定义Store的创建过程:

optionsStore模式

  1. options对象取出state、getters、actions

  2. 定义setup函数。

  3. 重写$reset方法。

  4. 创建partialStore基础store对象,将$patch$reset$subscribe等实例方法挂载到该对象上。

  5. 使用reactive(partialStore)方法创建响应式Store对象。

  6. 将创建好的store对象以键值对方式($id, store)注册到Pinia._s属性中。

  7. 调用setup()函数:设置pinia.state.value[id] = state(),然后将localState每个属性设置为ObjectRefImpl数据,将getters中的每个getter转换成计算属性,最后将处理完成后的state、getters、actions合并到一个对象中并返回,生成setupStore

  8. 循环setupStore对象,optionsStore模式在这里仅仅是包装一下actions中的方法。

  9. setupStore的内容合并到Store对象。

  10. 给Store对象定义一个$state访问器属性。

  11. 返回Store对象。

setupStore模式

  1. 初始化state,设置pinia.state.value[$id] = {}空对象。

  2. 创建partialStore基础store对象,将$patch$reset$subscribe等实例方法挂载到该对象上。

  3. 使用reactive(partialStore)方法创建响应式Store对象。

  4. 将创建好的store对象以键值对方式($id, store)注册到Pinia._s属性中。

  5. 调用setup()函数,初始化内部的ref/computed/reactive响应式数据,获取return返回的setupStore

  6. 循环setupStore对象,循环设置pinia.state.value[$id][key] = prop,同时包装一下actions中的方法。

  7. setupStore的内容合并到Store对象。

  8. 给Store对象定义一个$state访问器属性。

  9. 返回Store对象。

我们再打印查看一下,创建完成的Store实例的内容:

export const useMainStore = defineStore('main', {
  state: () => {
    return {
      count: 0
    }
  },
  actions: {
    addCount() {
      this.count++
    }
  }
})
const mainStore = useMainStore()
console.log(mainStore)

查看Store实例的内容:

image.png

4.扩展

最后我们再扩展一下,日常我们直接修改state和通过$patch方法修改state的原理区别。

还是按两种模式展示:

// options模式
const mainStore = useMainStore()
// setup模式
const userStore = useUserStore()

直接修改原理

optionsStore模式下:实际上是通过ObjectRefImpl数据内部代理的形式,实现对原对象的修改。

mainStore.count = 1

在前面我们就已经知道了optionsStore模式下的Store中有一行重要代码:

// pinia.state.value[id]  就是我们在mianStore刚开始的 state对象;{ count: 0 }
const localState = toRefs(pinia.state.value[id])

这里用toRefs方法:将原始的state对象作为参数,返回一个新对象,其属性都是转换后的ObjectRefImpl数据,了解toRefs方法源码的都知道,对ObjectRefImpl数据的设置,实际上都是会代理到原对象,即state对象。

class ObjectRefImpl<T extends object, K extends keyof T> {
  ...
  
  set value(newVal) {
    // 原理:修改原对象
    this._object[this._key] = newVal
  }
}

image.png

setupStore模式下:因为注册时引用的同一个ref数据对象,所以通过userStore.age的修改,pinia.state.value.user.age也会变化,因为它们指向了同一个对象。

userStore.age = 1

image.png

image.png

案例验证:

const userStore = useUserStore()
const p = mainStore._p
// 验证: 指向同一个对象
console.log(p.state.value.user.age === userStore.age) // true

$patch方法修改原理

$patch批量修改方法原理就比较简单了,两种模式都是一样的原理:

# 修改state
// 1,对象参数
store.$patch({
  count: store.count + 1,
  age: 120,
  name: 'DIO',
})
// 2,函数参数
store.$patch((state) => {
  state.items.push({ name: 'shoes', quantity: 1 })
  state.hasChanged = true
})
# patch方法
function $patch( partialStateOrMutator ): void {
  // 声明Mutation对象
  let subscriptionMutation: SubscriptionCallbackMutation<S>
  // 暂停订阅:避免修改state的过程中频繁触发回调
  isListening = isSyncListening = false
  
  # 参数为函数的情况:
  if (typeof partialStateOrMutator === 'function') {
    // 直接调用函数,参数为id对应的state数据
    partialStateOrMutator(pinia.state.value[$id])
      
    subscriptionMutation = {
      type: MutationType.patchFunction,
      storeId: $id,
      events: debuggerEvents as DebuggerEvent[],
    }
  } else {
    # 参数为对象的情况:
    // 调用mergeReactiveObjects方法,递归合并传入state,覆盖更新value[$id]的key值
    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
  // because we paused the watcher, we need to manually call the subscriptions
  // 重新订阅:触发回调
  triggerSubscriptions(
    subscriptions,
    subscriptionMutation,
    pinia.state.value[$id] as UnwrapRef<S>
  )
}
  • 参数为函数的情况下:直接调用函数,参数为目标对象,直接修改属性即可。
  • 参数为对象的情况下:递归处理,使用新对象的值,覆盖更新value[$id][key]的值。

storeToRefs

storeToRefs的实现很简单, 基本上就是将store中的值取出来并且转为ref, 然后返回.

export function storeToRefs<SS extends StoreGeneric>(
  store: SS
): StoreToRefs<SS> {
  // See https://github.com/vuejs/pinia/issues/852
  // It's easier to just use toRefs() even if it includes more stuff
  if (isVue2) {
    // @ts-expect-error: toRefs include methods and others
    return toRefs(store)
  } else {
    store = toRaw(store)

    const refs = {} as StoreToRefs<SS>
    for (const key in store) {
      const value = store[key]
      if (isRef(value) || isReactive(value)) {
        // @ts-expect-error: the key is state or getter
        refs[key] =
          // ---
          toRef(store, key)
      }
    }

    return refs
  }
}