
昨天我们看了cretePinia的逻辑,了解了初始化的逻辑。
今天我们来看下defineStore。
昨天我们用的测试用例是没有数据的,今天我们换一个:
这是官方提供的:
不过我一般的写法是setup stores:
所以我这里修改下测试用例
前面一大段的函数重载
我们来看下核心代码:
代码量不多,就不做删减了。
idOrOptions:我们defineStore的第一个参数,作为唯一标识符。setup:我们传入的第二个参数,我们这里传入了一个函数:isSetupStore: 不多说,我们的写法自然就是setup store,注意,只要传入函数就会被认定是setup store的写法。最后它返回一个useStore的函数,并把我们的唯一标识符赋值给这个函数$id。
而这个useStore的函数就是我们业务代码中引入的hook,我们需要执行这个hook才能拿到最终的数据:
这里我们不需要传入pinia作为参数,因为我们有全局activePinia。
然后我们来看下useStore执行的逻辑。
hasInjectionContext:这个函数来自于vue(vue-demi):inject逻辑,前面一章我们说过了currentInstance、currentRenderingInstsance的作用以及provide/inject的实现逻辑,所以这里不多说。pinia = inject(piniaSymbol, null):没啥好说的,从当前组件实例中拿到pinia,前面一章我们知道了install的时候这货会用vue的provide将自己注入所有组件实例中,所以理论上是拿得到的。if (pinia) setActivePinia(pinia):这里是将全局的activePinia设置成当前组件实例绑定的pinia实例。你可呢在疑惑为什么要这么做,install到vue根组件的时候不是已经这么做了吗?为什么还要再设置一次?因为不一定只有一个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热更新的原理,我们以后再分析,现阶段先跳过。
这货代码较长,我这里就不贴整体的了,我们拆开了一部分一部分分析:
函数整体:
可以看到最终是返回一个reactive包裹的响应式store数据,这个数据就是最终我们useXX返回的数据,对应前面useStore函数的返回store。
然后我们来看下这个partialStore:
_p:这里注意和pinia实例的_p区分开来,pinia实例的_p指向pinia的插件。这里则指向pinia实例。然后没啥好说的了,这就是在初始化一个store实例。
我们接着看下每个property是干啥用的。
$onAction
首先是犯下傲慢的$onAction:Interface: _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返回的就是一个reactive的store实例,所以这里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)。
则例isListening和isSyncListening俩参数暂时还不清楚是干什么用的,所以先暂时搁置,后面遇到再解释。
triggerSubscriptions:就是简单的执行回调,注意是slice是clone一个,而不是直接占用。
这里args里有俩参数:
注意这里第一个参数里有一个debuggerEvents,只有开发阶段才能使用,我们可以往里面传入debug回调。
相关链接:Interface: SubscriptionCallbackMutationPatchFunction | Pinia (vuejs.org)
到这我们暂时只知道如何存储,还不知道如何调用,等会遇到了我们再说。
ok, $patch分析到这,我们有俩个点还没分析:
然后我们来看下$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 unlessdetachedis set to true.
$onAction不同的是这里还传入了一个() => stopWatcher()作为addSubscription的第四个参数onCleanup,在执行函数返回的函数即注销回调会执行这个onCleanup。触发stopWatcherstopWatcher中有个scope,我们来看下这货是从哪来的:_a:这货前面说过了,就是这个pinia实例注册的那个vue根实例runWithContext:这个则是vue里的函数,用来确保当前执行函数的上下文是对应的vue根实例。_e: EffectScope,这货是pinia实例初始化即createPinia时触发的effectscope。简单的说就是创建一个当前store的effectScope,用来收集store(我们传入的setup函数)里的响应式数据
然后回到stopWatcher中,scope收集一个watch,watch的作用是只要当前store实例响应式数据发生变化就执行回调。注意是当前store实例,而$onAction则是任一store实例执行action(即$patch)之后。
这里我们遇到了前面没说的isSyncListening和isListening字段
然后我们结合$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只能在flush和post两种情况下执行,记录isListening是确保不会执行错回调,毕竟这个回调只能在这个store实例数据刷新后执行。
ok,还剩最后一个$dispose
$dispose
Interface: _StoreWithState | Pinia (vuejs.org)
这货不用多说,就是remove这个store实例。
ok,跳出partialStrore回到我们的createSetupStore函数中
在执行了runWithContext之后我们拿到了setupStore,为了方便分析,我加了点其它的东西
然后我们接着看代码(部分dev和hmr相关的代码省略):
shouldHydrate:是否需要注入,如果是对象并且不在store中,那就需要合并mergeReactiveObjects:篇幅问题,这里就不贴代码了,就是在合并响应式数据wrapAction:这货得看下代码:普通函数的。简单的说就是包裹一层普通函数,让它也能触发action回调以及函数自己的回调。然后继续回到createSetupStore中,最终
注意这里的store是前面基于partialStore创建的store,没有数据的,而setupStore则是数据。
接着我们继续往下看:
这是在触发插件并把结果赋值给store。
最后一部分:
这里为什么要把isListening和isSyncListening置为true,因为我们的$patch和$subscribe以及watch回调实际上是闭包,如果这里不变成true,那么等这个函数出栈,里面的逻辑就有问题了。
ok,createSetupStore总算是讲完了,我们整理下。
简单整理createSetupStore
partialStore(里面包含比如$patch的api)创建reactive store实例,然后将它放入pinia._s(Map<string, StoreGeneric>)中。vue.runWithContext在这个pinia实例对应的vue上下文中执行setup函数获得我们的setupStore也就是store数据setupStore,如果是普通函数则包裹一层用于触发action。setupStore给store实例store实例isListening和isSyncListening置为true,避免函数推出调用栈导致闭包中引用有问题store实例。然后我们再来讲下createOptionsStore
这货就比较简单了,因为格式有板有眼,直接贴代码:
这里唯一要说的是computedGetter。
先看下getter的基础用法:
回归性原理:getters就是computed!
这里还有一点就是markRaw,说明getter数据改动不会引起响应式更新。
其它没啥好说的了。。。
我是没想到这一章内容这么多。。。。
简单总结下defineStore里做了什么:
setupStore还是optionsStoreuseStore函数,这个函数就是我们业务代码调用时的const xx = useXX()的useXX。useStore里首先将上下文调整为符合当前pinia实例的vue根实例setupStore分别调用createSetupStore和createOptionsStorestore实例突然发现没了,我们要分析的内容到这里就结束了~