坏蛋Dan
知乎@坏蛋Dan
发布时间:2024.6.21

前言

昨天我们看了cretePinia的逻辑,了解了初始化的逻辑。

今天我们来看下defineStore


defineStore

切换测试用例

昨天我们用的测试用例是没有数据的,今天我们换一个:

这是官方提供的:

不过我一般的写法是setup stores

所以我这里修改下测试用例

源码

前面一大段的函数重载

我们来看下核心代码:

代码量不多,就不做删减了。

  • idOrOptions:我们defineStore的第一个参数,作为唯一标识符。
  • setup:我们传入的第二个参数,我们这里传入了一个函数:
  • isSetupStore: 不多说,我们的写法自然就是setup store,注意,只要传入函数就会被认定是setup store的写法。

最后它返回一个useStore的函数,并把我们的唯一标识符赋值给这个函数$id

而这个useStore的函数就是我们业务代码中引入的hook,我们需要执行这个hook才能拿到最终的数据:

这里我们不需要传入pinia作为参数,因为我们有全局activePinia

然后我们来看下useStore执行的逻辑。

  • hasInjectionContext:这个函数来自于vue(vue-demi)

    查看是否可以执行inject逻辑,前面一章我们说过了currentInstancecurrentRenderingInstsance的作用以及provide/inject的实现逻辑,所以这里不多说。
  • pinia = inject(piniaSymbol, null):没啥好说的,从当前组件实例中拿到pinia,前面一章我们知道了install的时候这货会用vueprovide将自己注入所有组件实例中,所以理论上是拿得到的。
  • if (pinia) setActivePinia(pinia):这里是将全局的activePinia设置成当前组件实例绑定的pinia实例。你可呢在疑惑为什么要这么做,installvue根组件的时候不是已经这么做了吗?为什么还要再设置一次?因为不一定只有一个vue根组件,也不一定只有一个pinia实例。可能存在两个pinia实例,比如:

    这就是俩pinia实例,另外这个项目中可能存在多个vue根组件实例,每个实例单独use一个pinia实例。但是我们只有一个activePinia位置!这玩意儿全局只有一个!。那么这个时候activePinia指向就不一定准了。
  • pinia._s_s: Map<string, StoreGeneric>,存放的是defineStore里的数据,key自然就是defineStore第一个参数。
  • createSetupStore: 这玩意儿代码有些长。。我们等会开个小章节说下,这里先跳过
  • createOptionsStore:这个和createSetupStore放到一起说,所以这里也先跳过
  • const store: StoreGeneric = pinia._s.get(id)!:这货就是我们useXX返回的数据,指向我们定义的store数据

剩余部分的代码是和开发阶段热更新有关的,官方链接:HMR (Hot Module Replacement) | Pinia (vuejs.org)

这里简单说下:如果是vite,那么开箱即用,vite支持自定义热更新内容(当然,webpack等也支持,不过写法不一样):HMR API | Vite (vitejs.dev)

这么做就可以搭vite热更新的车了。

具体如何实现的,因为涉及到vite热更新的原理,我们以后再分析,现阶段先跳过。

createSetupStore

这货代码较长,我这里就不贴整体的了,我们拆开了一部分一部分分析:

函数整体:

可以看到最终是返回一个reactive包裹的响应式store数据,这个数据就是最终我们useXX返回的数据,对应前面useStore函数的返回store

然后我们来看下这个partialStore

  • _p:这里注意和pinia实例_p区分开来,pinia实例_p指向pinia的插件。这里则指向pinia实例

然后没啥好说的了,这就是在初始化一个store实例。

我们接着看下每个property是干啥用的。

$onAction

首先是犯下傲慢的$onActionInterface: _StoreWithState | Pinia (vuejs.org)

就是一个简单的订阅和取消订阅的方法,这里只是订阅,还没涉及到发布。

所以这个$onAction就是用来订阅的,另外根据官方文档的描述:

Setups a callback to be called every time an action is about to get invoked

这货可以用在每次action触发后的回调。

返回一个函数用来手动注销回调。

看下基础用法:

然后我们再来看下$patch

$patch

Interface: _StoreWithState | Pinia (vuejs.org)

在看代码之前,我们先看下基础用法:

然后看下代码

代码稍微复杂了点。

首先,如果传入的是函数,那么会将store实例的state传递给我们传入和函数,否则直接合并俩state。。前面一直没有解释类型,因为可读性较差,另外是觉得没必要,现在是时候看下UnwrapRef<T>了:

不是我不想解释,实在是太类型体操了。

简单的说下UnwrapRef<T>的作用,就是用来取消ref包裹数据的那一层。

原本我们需要调用.value才能访问到这个数据,使用UnwrapRef<T>之后就不需要了。

来看下官方的例子:Reactivity Fundamentals | Vue.js (vuejs.org)

当它被赋值给一个reactive的属性的时候它会自动解除外面那一层。

前面我们知道了usexx返回的就是一个reactivestore实例,所以这里state自然就是UnwrapRef

不过这里你可能会有疑惑,为什么传给我们回调的是pinia.state.value[$id]

这就需要我们回到createPinia那里:

这里的state就是上面的pinia.state,因为pinia本身是raw的,所以state并不能变成UnwrapRef

ok,但是这里还有个问题,初始化的时候是空的,这里却是从里面拿出store来,这不对啊。

实际上前面还有逻辑我没给出:

就是在这里,因为我们传给$patch的是函数,所以我们是自己忘state上面加数据的,所以这里只需要判断是否为空,为空初始化这个实例在state上的数据即可。

那么到这我们就知道了为什么这里是从pinia.state.value[$id]中拿到的数据。

貌似扯的有些远了,我们回到之前的代码,在执行完我们传入的函数之后。

我们前面看的$onAction就是设置回调可以在每次action之后执行回调,那么这里我们在更新数据之后就需要触发回调(pinia只有action)。

则例isListeningisSyncListening俩参数暂时还不清楚是干什么用的,所以先暂时搁置,后面遇到再解释。

  • triggerSubscriptions

就是简单的执行回调,注意是sliceclone一个,而不是直接占用。

这里args里有俩参数:

注意这里第一个参数里有一个debuggerEvents,只有开发阶段才能使用,我们可以往里面传入debug回调。

相关链接:Interface: SubscriptionCallbackMutationPatchFunction | Pinia (vuejs.org)

到这我们暂时只知道如何存储,还不知道如何调用,等会遇到了我们再说。

ok, $patch分析到这,我们有俩个点还没分析:

  1. debuggerEvents
  2. isListening以及isSyncListening

然后我们来看下$reset

$reset

Interface: _StoreWithState | Pinia (vuejs.org)

Resets the store to its initial state by building a new state object.

不多说,直接看代码

可以看到只有使用optionStore的时候才可行,为什么呢?

因为我们用的是setup语法,没有state函数。

总之,setupStore需要开发者手动实现初始化函数,因为不像optionStore一样是一个对象很好管理。

然后我们来看下$subscribe

$subscribe

Interface: _StoreWithState | Pinia (vuejs.org)

Setups a callback to be called whenever the state changes. It also returns a function to remove the callback. Note that when calling store.$subscribe() inside of a component, it will be automatically cleaned up when the component gets unmounted unless detached is set to true.

  1. 先将我们的回调放入到订阅器中,不过和前面$onAction不同的是这里还传入了一个() => stopWatcher()作为addSubscription的第四个参数onCleanup,在执行函数返回的函数即注销回调会执行这个onCleanup。触发stopWatcher
  2. stopWatcher中有个scope,我们来看下这货是从哪来的:
  • _a:这货前面说过了,就是这个pinia实例注册的那个vue根实例
  • runWithContext:这个则是vue里的函数,用来确保当前执行函数的上下文是对应的vue根实例。
  • _e EffectScope,这货是pinia实例初始化即createPinia时触发的effectscope

简单的说就是创建一个当前storeeffectScope,用来收集store(我们传入的setup函数)里的响应式数据

然后回到stopWatcher中,scope收集一个watchwatch的作用是只要当前store实例响应式数据发生变化就执行回调。注意是当前store实例,而$onAction则是任一store实例执行action(即$patch)之后。

这里我们遇到了前面没说的isSyncListeningisListening字段

然后我们结合$patch里的逻辑,整理下:

这里需要补充watch的知识:Watchers | Vue.js (vuejs.org)

简单的说,就是用来解决大数据量操作时同步触发大量watch回调的问题,所以vue针对这点增加了watch的这一特性,可以让开发者定义什么时候执行watch回调。

默认情况下,watch回调是在父组件更新后子组件DOM更新前触发(flush: sync),这么做是避免watch里改动数据引起重复渲染。

当然,如果你有这方面的想法,想在子组件(当前组件)更新之后再执行,可以设置flush: post

或者说在这之前获取:

ok,回到我们的代码中。在知道了watch可以手动调整执行回调timing之后,我们知道了这里nextTick + activeListener === myListenerId的作用,就是用来判断当前是否是flush: post执行,如果是,那么isListening = true

$subscribe里的watch只能在flushpost两种情况下执行,记录isListening是确保不会执行错回调,毕竟这个回调只能在这个store实例数据刷新后执行。

ok,还剩最后一个$dispose

$dispose

Interface: _StoreWithState | Pinia (vuejs.org)

这货不用多说,就是remove这个store实例

ok,跳出partialStrore回到我们的createSetupStore函数中

在执行了runWithContext之后我们拿到了setupStore,为了方便分析,我加了点其它的东西

然后我们接着看代码(部分devhmr相关的代码省略):

  • shouldHydrate:是否需要注入,如果是对象并且不在store中,那就需要合并
  • mergeReactiveObjects:篇幅问题,这里就不贴代码了,就是在合并响应式数据
  • wrapAction:这货得看下代码:

    它是用于普通函数的。简单的说就是包裹一层普通函数,让它也能触发action回调以及函数自己的回调。

然后继续回到createSetupStore中,最终

注意这里的store是前面基于partialStore创建的store,没有数据的,而setupStore则是数据。

接着我们继续往下看:

这是在触发插件并把结果赋值给store

最后一部分:

这里为什么要把isListeningisSyncListening置为true,因为我们的$patch$subscribe以及watch回调实际上是闭包,如果这里不变成true,那么等这个函数出栈,里面的逻辑就有问题了。

ok,createSetupStore总算是讲完了,我们整理下。

简单整理createSetupStore

  1. 基于partialStore(里面包含比如$patch的api)创建reactive store实例,然后将它放入pinia._s(Map<string, StoreGeneric>)中。
  2. 调用vue.runWithContext在这个pinia实例对应的vue上下文中执行setup函数获得我们的setupStore也就是store数据
  3. 遍历合并setupStore,如果是普通函数则包裹一层用于触发action
  4. 赋值setupStorestore实例
  5. 触发插件,把结果合并到store实例
  6. isListeningisSyncListening置为true,避免函数推出调用栈导致闭包中引用有问题
  7. 最后返回store实例

然后我们再来讲下createOptionsStore

createOptionsStore

这货就比较简单了,因为格式有板有眼,直接贴代码:

这里唯一要说的是computedGetter

先看下getter的基础用法:

回归性原理:getters就是computed

这里还有一点就是markRaw,说明getter数据改动不会引起响应式更新。

其它没啥好说的了。。。


总结

我是没想到这一章内容这么多。。。。

简单总结下defineStore里做了什么:

  1. 区分你是setupStore还是optionsStore
  2. 返回useStore函数,这个函数就是我们业务代码调用时的const xx = useXX()useXX
  3. useStore里首先将上下文调整为符合当前pinia实例vue根实例
  4. 根据是否是setupStore分别调用createSetupStorecreateOptionsStore
  5. 返回上面俩函数执行拿到的store实例
  6. 俩函数的逻辑上面总结了,这里就不总结了。

突然发现没了,我们要分析的内容到这里就结束了~