Analyzing Pinia Source Code ✨
Mon Oct 17 2022 5 months ago
前言 #
最近翻看vue
的rfcs提案时,忽然看到vuex5.0
的提案,看到社区也有很多的探索讲解,于是我想给大家来点干货,顺便记录下我学习pinia
的过程。
5.0的提案非常具有新鲜感,对比
vuex4
具有很大的改进
- 支持
options api
andcomposition api
- 没有
mutations
- 没有嵌套的模块
- 更好
typescript
支持 - 自动化的代码差分
于是我fork
的一份代码,为了充分的理解pinia
的流程,我在examples
文件夹下使用webpack
搭建了一个本地服务进行代码调试,欢迎大家clone
和debug
备注:pinia version: 2.0
我的git地址:https://github.com/zyyv/learn-pinia
以下直接进行源码拆分讲解,api部分参考官网
入口-createpinia #
假设你阅读或使用过pinia
,我们知道pinia
的入口是通过createPinia
创建的Pinia
实例,当我们在vue
中使用时,使用app.use(pinia)
加载pinia
import { createApp } from 'vue'import { createPinia } from 'pinia'import App from './App.vue'const pinia = createPinia()const app = createApp(App).use(pinia)app.mount('#app')
看下源码实现
export function createPinia(): Pinia { const state = ref({}) let localApp: App | undefined const _p: Pinia['_p'] = [] const toBeInstalled: PiniaStorePlugin[] = [] // 这是当前的pinia实例 const pinia: Pinia = markRaw({ install(app: App) { // pinia 通过vue的插件机制,暴露对外的install方法 pinia._a = localApp = app app.provide(piniaSymbol, pinia) // 通过provide提供pinia实例,供后续使用 app.config.globalProperties.$pinia = pinia // 暴露全局属性 $pinia if (IS_CLIENT) setActivePinia(pinia) // 设置当前活跃的 pinia toBeInstalled.forEach(plugin => _p.push(plugin)) // 加载pinia插件 }, use(plugin) { // 这是pinia暴露的插件用法 if (!localApp) toBeInstalled.push(plugin) // 将插件存入[],待初始化的时候使用 else _p.push(plugin) return this }, _p, _a: localApp!, // app 实例 state, // 所有状态 }) return pinia}
详细注释已标明
可以看到 pinia 实例
拥有state = ref({})
这其实是所有的state的集合,后面会在init
的时候,将其他模块的state
挂载到pinia
下
其实pinia
也更好的集成了 vue devtools
if (__DEV__ && IS_CLIENT) pinia.use(devtoolsPlugin)
定义store-definestore #
我们回顾一下 defineStore Api
,可以看到,使用defineStore
需要传入一个options
配置,定义每一个store
import { defineStore } from 'pinia'// useStore could be anything like useUser, useCartexport const useStore = defineStore({ // unique id of the store across your application id: 'storeId',})
我们来看下源码是如何写的呢?
可以清晰的看到,defineStore
简单的返回定义好的useStore
,并标记唯一$id
我们看看内部的useStore
是如何处理传入的options
个人见解:我将
useStore
分为4部分处理,下面逐一讲解
初始化形参pinia #
粘贴部分代码讲解
const currentInstance = getCurrentInstance()const shouldProvide = currentInstance && !piniapinia = (__TEST__ && activePinia && activePinia._testing ? null : pinia) || (currentInstance && inject(piniaSymbol))
if (shouldProvide) provide(storeAndDescriptor[2], store)
首先通过vue
的getCurrentInstance
拿到当前的vue实例,并判断形参的pinia
是否存在,以后须判断是否需要向children
提供当前的store
这里提前讲有点懵,可以先略过,稍后再回顾
pinia
如果没有则会通过inject
获取,因为在 app.use
的时候,install
方法内已经提供了
设置activepinia #
if (pinia) setActivePinia(pinia)pinia = getActivePinia()
主要是设置当前活跃的是哪个pinia
实例,当有多个pinia
实例时,方便获取当前活跃的pinia
实例
export let activePinia: Pinia | undefinedexport const setActivePinia = (pinia: Pinia | undefined) => (activePinia = pinia)export const getActivePinia = () => { if (__DEV__ && !activePinia) { warn( '[🍍]: getActivePinia was called with no active Pinia. Did you forget to install pinia?\n\n' + 'const pinia = createPinia()\n' + 'app.use(pinia)\n\n' + 'This will fail in production.', ) } return activePinia!}
添加store缓存 #
export const storesMap = new WeakMap<Pinia,Map<string,[ StoreWithState<string, StateTree>, StateDescriptor<StateTree>, InjectionKey<Store>,]>>()
首先会导入一个storesMap
,它的数据结构为WeakMap
,key
时一个pinia
实例,value
是一个Map
结构
let storeCache = storesMap.get(pinia)if (!storeCache) storesMap.set(pinia, (storeCache = new Map()))
先通过pinia
作为key
取store
的缓存,如果缓存不存在,那么便设置一个新的Map
let storeAndDescriptor = storeCache.get(id)let store: Store<Id, S, G, A>if (!storeAndDescriptor) { // 下面传入的参数:{options.id, options.state, 还记得pinia实例的state吗shi?是个Ref对象} storeAndDescriptor = initStore(id, state, pinia.state.value[id]) storeCache.set(id, storeAndDescriptor) ...} else { ...}
可以清晰看到,storeCahe
通过id
获取store的缓存与store的一些描述符(storeAndDescriptor
)
当我们没有获取到storeCahe
时,会进行initStore
的操作,并且可以看出initStore
的返回结果,就是我们想要的storeAndDescriptor
,并重新添加到缓存里面
initstore #
先看看 initStore 的参数与返回值
function initStore< Id extends string, S extends StateTree, G extends GettersTree<S>, A, /* extends ActionsTree */>( $id: Id, // 每一个模块的id buildState: () => S = () => ({} as S), // 模块的state initialState?: S | undefined, // pinia实例下`{id}`的状态): [ StoreWithState<Id, S, G, A>, { get: () => S, set: (newValue: S) => void }, InjectionKey<Store>, ] { // .. someCode}
形参在注释中标注,而我们可以看到返回值这一块,它将返回一个数组格式,
StoreWithState
这是交给外部使用的store
实例- 第二位其实是
state
的属性描述符 - 这是一个需要
provide
提供的InjectionKey
然后看程序主体这一块
const pinia = getActivePinia()pinia.state.value[$id] = initialState || buildState()
拿到当前活跃的pinia
, 将模块的state
通过id
挂在到pinia.state
下面
后面是定义了一些变量,有我们经常使用的patch
函数,然后是一些依赖的收集与触发,我们留到下一章再讲
const storeWithState: StoreWithState<Id, S, G, A> = { $id, _p: pinia, _as: actionSubscriptions as unknown as StoreOnActionListener[], // $state is added underneath $patch, $subscribe, $onAction, $reset,} as StoreWithState<Id, S, G, A>const injectionSymbol = __DEV__ ? Symbol(`PiniaStore(${$id})`) : /* istanbul ignore next */ Symbol()
我们可以看到storeWithState
的完整形态,它包含了一些属性与方法暴露给外部使用
而我们的injectionSymbol
是一个包含$id
的Symbol
类型
return [ storeWithState, { get: () => pinia.state.value[$id] as S, set: (newState: S) => { isListening = false pinia.state.value[$id] = newState isListening = true }, }, injectionSymbol,]
值得注意的是,数组的第二位是我们descriptor
,主要是对state的获取与设置,因为我们可以通过在pinia
实例上通过id
拿到模块的state
最后返回用数组包装的数据。initStore
结束
buildstoretouse #
回到defineStore
的过程,当我们initStore
结束,拿到storeAndDescriptor
,会进行一个设置缓存的动作(上面有提到)
那么store
到底是什么数据格式呢,其实还是要通过buildStoreToUse
包装一下
store = buildStoreToUse<Id,S,G,// @ts-expect-error: A without extendsA>( storeAndDescriptor[0], // storeWithState storeAndDescriptor[1], // descriptor id, // options.id getters, // options.getters actions, // options.actions options,)
那我们来看看是如何包装的把
getters
首先拿到当前活跃的pinia
实例
const pinia = getActivePinia()const computedGetters: StoreWithGetters<G> = {} as StoreWithGetters<G>for (const getterName in getters) { computedGetters[getterName] = computed(() => { setActivePinia(pinia) return getters[getterName].call(store, store) }) as StoreWithGetters<G>[typeof getterName]}
可以看到,使用了for in
循环处理我们的配置的getters
,同时,getters
的key
缓存到了computedGetters
里面,并且使用computed
包裹,实现了真正的计算属性。对getters
通过call
绑定this
到store
,并传入store
到getters
内
actions
const wrappedActions: StoreWithActions<A> = {} as StoreWithActions<A>for (const actionName in actions) { wrappedActions[actionName] = function(this: Store<Id, S, G, A>) { setActivePinia(pinia) const args = Array.from(arguments) as Parameters<A[typeof actionName]> const localStore = this || store // 兼容箭头函数处理 let afterCallback: ( resolvedReturn: UnwrapPromise<ReturnType<A[typeof actionName]>> ) => void = noop let onErrorCallback: (error: unknown) => void = noop function after(callback: typeof afterCallback) { afterCallback = callback } function onError(callback: typeof onErrorCallback) { onErrorCallback = callback } partialStore._as.forEach((callback) => { callback({ args, name: actionName, store: localStore, after, onError }) }) let ret: ReturnType<A[typeof actionName]> try { ret = actions[actionName].apply(localStore, args as unknown as any[]) Promise.resolve(ret).then(afterCallback).catch(onErrorCallback) } catch (error) { onErrorCallback(error) throw error } return ret } as StoreWithActions<A>[typeof actionName]}
同样看到使用for in
循环处理我们的actions
,actions
的key
处理到了wrappedActions
里面,
当我们触发action
时,首先会设置最新的pinia
实例。定义了一个localStore
,并对其做了一个兼容处理,当时action
为箭头函数时,localStore
会指向store
。
partialStore._as.forEach((callback) => { callback({ args, name: actionName, store: localStore, after, onError })})
然后action
的触发会对收集到的依赖进行发布
let ret: ReturnType<A[typeof actionName]>try { ret = actions[actionName].apply(localStore, args as unknown as any[]) Promise.resolve(ret).then(afterCallback).catch(onErrorCallback)} catch (error) { onErrorCallback(error) throw error}return ret
可以看到action
的触发通过apply
的方式传参,绑定this
为localStore
,同时它讲arguments
数组化作为payload
传入第二个参数,这意味着,在我们的actions
中,可以有多个payload
进行传递
我们actions
执行的结果保存到ret
中,再用promise
包裹一层,最后我们返回原始的ret
值。其实在这里我们可以看到,Pinia
实现了将actions
同步和异步的共同处理。对比vuex
,actions
是处理异步任务的配置项,返回结果用promise
包装。而Pinia
则是直接返回actions
的返回值,通过promise
进行事件循环的微任务执行,达到异步的处理。
store
const store: Store<Id, S, G, A> = reactive( assign( __DEV__ && IS_CLIENT ? // devtools custom properties { _customProperties: markRaw(new Set<string>()), } : {}, partialStore, // using this means no new properties can be added as state computedFromState(pinia.state, $id), computedGetters, wrappedActions, ),) as Store<Id, S, G, A>
然后处理我们的store,其实是使用了Object.assign
进行混合
其实我们已经理解到store
的state
、getters
都是用computed
进行包装。使得我们可以直接对state
进行直接的修改,对比vuex
的mutations
修改,操作上也是简化了不少
Object.defineProperty(store, '$state', descriptor)
然后给store
的$state
添加我们传过来的属性描述符
最后返回store
,buildStoreToUse
结束。
if (shouldProvide) provide(storeAndDescriptor[2], store)
回到最初的shouldProvide
,它决定是否允许child
重用这个store
以避免创建一个新的store
总结 #
第一章,简单介绍了入门篇章,后面将持续讲解,不定期更新。
以上及以下是我对Pinia
的个人理解,如果不足或失误,请指正。