专栏导航 分析pinia源码之前必须知道的API
Pinia源码分析【1】- 源码分析环境搭建
Pinia源码分析【2】- createPinia
pinia源码分析【3】- defineStore
pinia源码分析【4】- Pinia Methods
前言 本系列文章参考源码pinia V2.0.14
源码分析记录:https://github.com/vkcyan/goto-pinia
在上一节,我们完成了createPinia
相关逻辑的源码解读,了解了pinia
注册到vue
的阶段具体做了哪些工作,以及pinia
核心对象的生成逻辑,本文我们就要一起解读pinia
中最重要的方法defineStore 的实现原理
关于store的初始化 三种创建方法
源码中对defineStore
的三种类型描述便解释了为何我们可以用以上三种方式创建。
在defineStore
声明中,我们需要传入三种的参数。
id :定义store
的唯一id,单独传参或者通过options.id
进行传参
options :具体配置信息包含如果传参是对象,则可以传,state
,getters
,action
,id
,例如上图1 2 种声明方式;如果传参是Function
,则自主声明变量方法,例如上图第三种声明方式
storeSetup :仅限第三种store
的声明方式,传入函数
defineStore执行逻辑 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 export function defineStore ( idOrOptions: any, setup?: any, setupOptions?: any ): StoreDefinition { let id : string; let options : const isSetupStore = typeof setup === "function" ; if (typeof idOrOptions === "string" ) { id = idOrOptions; options = isSetupStore ? setupOptions : setup; } else { options = idOrOptions; id = idOrOptions.id ; } function useStore (pinia?: Pinia | null , hot?: StoreGeneric ): StoreGeneric { } useStore.$id = id; return useStore; }
通过对defineStore
的源码大致分析可以得知,只有在store
被执行的时候才会运行被返回的函数useStore
,useStore
才是核心store的创建逻辑,我们接下便要重点分析其实现原理。
useStore逻辑分析 useStore之前的逻辑执行顺序 我们在App.vue
中使用我们创建的store
1 2 3 <script setup lang ="ts" > const useCounter1 = useCounterStore1 (); </script >
在main
createPinia
defineStore
useStore
初始化处增加日志
defineStore
初始化
main.ts -> createPinia -> vue.use -> install(注册逻辑)
执行useStore(页面逻辑)
代码执行与我们想象的一致,defineStore
是一个函数,会在引用阶段执行,并返回未执行函数useStore
,之后便是一连串的初始化,最后是页面中使用pinia
而运行的useStore
。
useStore准备工作
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 function useStore (pinia?: Pinia | null , hot?: StoreGeneric ): StoreGeneric { const currentInstance = getCurrentInstance (); pinia = (__TEST__ && activePinia && activePinia._testing ? null : pinia) || (currentInstance && inject (piniaSymbol)); if (pinia) setActivePinia (pinia); if (__DEV__ && !activePinia) { throw new Error ( `[🍍]: getActivePinia was called with 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!; }
核心store创建
当我们第一次运行store
的时候,才会进行相关逻辑的执行,通过单例模式创建,未来再次使用该store
将会直接从pinia._s
中获取已经被处理过的store并返回。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 function useStore (pinia?: Pinia | null , hot?: StoreGeneric ): StoreGeneric { if (!pinia._s .has (id)) { if (isSetupStore) { createSetupStore (id, setup, options, pinia); } else { createOptionsStore (id, options as any, pinia); } } const store : StoreGeneric = pinia._s .get (id)!; return store as any; }
useStore
的大致逻辑比较简单,我们假设第一次使用,并且通过非Function进行传参,进入createOptionsStore 函数。
createOptionsStore defineStore
的第二个参数使用非Function
进行声明将会走入该逻辑
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 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]; console .log ("initialState" , initialState); let store : Store <Id , S, G, A>; function setup ( ) { if (!initialState && (!__DEV__ || !hot)) { if (isVue2) { set (pinia.state .value , id, state ? state () : {}); } else { pinia.state .value [id] = state ? state () : {}; } } console .log (11 , pinia.state .value [id]); console .log (22 , toRefs (pinia.state .value [id])); const localState = __DEV__ && hot ? toRefs (ref (state ? state () : {}).value ) : toRefs (pinia.state .value [id]); let aa = 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} ".` ); } computedGetters[name] = markRaw ( computed (() => { setActivePinia (pinia); const store = pinia._s .get (id)!; if (isVue2 && !store._r ) return ; return getters![name].call (store, store); }) ); return computedGetters; }, {} as Record <string, ComputedRef >) ); console .log ("aa" , aa); return aa; } store = createSetupStore (id, setup, options, pinia, hot, true ); store.$reset = function $reset ( ) { const newState = state ? state () : {}; this .$patch(($state ) => { assign ($state, newState); }); }; return store as any; }
createOptionsStore
函数在获取defineStore
声明的数据后,在其内部构建了setup函数 ,该函数将option形式的state 与getters 分别转化为ref 与computed ,这样就与setup形式 声明的store
保持一致。
这一块代码非常核心,初步解释了为何state具备响应式,为何getters具备computed的效果
最后不论是option 方式创建还是setup 的形式创建,最后都统一通过createSetupStore
完成对store
最后的处理
createSetupStore
无论是何种defineStore
创建方式,最终都会走向createSetupStore
,在这里进行最终store的生成以及相关methods的实现。
注:这一块代码实在是复杂,关于$reset $patch等API,我们放下一个系列文章
经过createOptionsStore
的处理,已经将option 形式的字段全部转化为setup 形式进行返回,现在无论何种创建方式,执行此处的setup函数,都会得到同一个结果。
以上三种创建方式,内部运行setup函数都会得到如下结果
接下来,我们就需要对其数据进行处理,获取到所有变量与方法,并对action通过wrapAction进行处理,便于实现后续的订阅发布模式 methods$Action
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 const setupStore = pinia._e .run (() => { scope = effectScope (); return scope.run (() => setup ()); })!; for (const key in setupStore) { const prop = setupStore[key]; if ((isRef (prop) && !isComputed (prop)) || isReactive (prop)) { if (!isOptionsStore) { if (isVue2) { set (pinia.state .value [$id], key, prop); } else { pinia.state .value [$id][key] = prop; } } } else if (typeof prop === "function" ) { const actionValue = __DEV__ && hot ? prop : wrapAction (key, prop); if (isVue2) { set (setupStore, key, actionValue); } else { setupStore[key] = actionValue; } optionsForPlugin.actions [key] = prop; } }
经过以上逻辑处理后,setupStore
方式进行创建的store
也会被添加到pinia.state
中,而所有的function
都会被wrapAction
进行包装处理。
对state,action进行处理的同时,还需要对当前store
可调用API进行处理,例如$reset
,$patch
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 const partialStore = { _p :pinia, $id, $Action, $patch, $reset, $subscribe, $dispose } const store : Store <Id , S, G, A> = reactive ( assign ( __DEV__ && IS_CLIENT ? { _customProperties : markRaw (new Set <string>()), _hmrPayload, } : {}, partialStore ) ) as unknown as Store <Id , S, G, A>; assign (toRaw (store), setupStore);
最终将相关methods
与store
内的数据进行合并,存储以当前store的id为key的Map
中,createSetupStore
的核心逻辑便全部结束了。
useStore后续逻辑 我们再回到defineStore
的逻辑中,获取到createSetupStore
最后放入pinia._s
中的当前store被处理后的对象。
1 2 3 4 5 const store : StoreGeneric = pinia._s .get (id)!;return store as any;
最后将通过createSetupStore
处理后的数据进行返回,我们便得到了使用当前store
中变量与方法以及各种方法的能力。
拓展:为什么访问defineStore创建的state不需要.value 通过以上源码分析可以得知,state的数据都会被处理为ref,那访问ref自然是需要.value,但是我们日常使用pinia似乎从来没有.value。
我们先看一个小例子
1 2 3 4 5 6 7 let name = ref ("张三" );let age = ref ("24" );const info = reactive ({ name, age });console .log (info.name ); console .log (info.age );
简单来说就是reactive中嵌套ref的时候,修改reactive内的值不需要.value
在官方文档(https://vuejs.org/api/reactivity-core.html#reactive)中,我们也能找到相关说明
注意:reactive嵌套ref的场景下,对象与数组格式存在差异,有兴趣可以了解一下
根据文档我们简单的翻阅了一下vuejs/core/…/baseHandlers.ts的源码
源码地址:https://github.com/vuejs/core/blob/main/packages/reactivity/src/baseHandlers.ts
line 131 - 134 createGetter()
line 131 - 134 createSetter()
可以发现,逻辑实现与文档描述相符。
最后再看一下我们的pinia源码中createSetupStore
函数中store
声明的那一段函数,这便解释了为什么在使用pinia
修改值、读取值的时候都不需要进行.value了。
1 2 3 4 5 6 7 8 9 10 11 12 13 const store : Store <Id , S, G, A> = reactive ( assign ( partialStore ) ) as unknown as Store <Id , S, G, A>; if (isVue2) { } else { assign (toRaw (store), setupStore); }
结语 虽然代码量比较大,但是核心逻辑就是将state处理为ref,将getters处理为computed,将action进行二次封装,提供若干方法,最后组合对象存储到Pinia中。
在这一章我们完成了最核心store
创建流程的源码分析,但是通过partialStore
增加的方法我们还没有一一了解。下一篇我们将会重点介绍store
相关Methods
的具体实现。