文章基于Pinia版本是:2.0.3
看源码之前,先了解下Pinia的用法浅谈Pinia(皮尼亚)--为什么vue3推荐使用Pinia
1. 入口 createPinia
从项目根目录可以看到,pinia
是基于rollup
打包的,找到rollup.config.js
, 发现入口是src/index.ts
在业务测是通过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')
打开packages/pinia/src/index.ts
找到createPinia
方法
路径(packages/pinia/src/createPinia.ts
)
/**
* Creates a Pinia instance to be used by the application
*/
export function createPinia(): Pinia {
const scope = effectScope(true)
// 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>>({}))!
let _p: Pinia['_p'] = []
// plugins added before calling app.use(pinia)
let toBeInstalled: PiniaPlugin[] = []
// 当前的pinia实例
const pinia: Pinia = markRaw({
install(app: App) { // 这是vue的插件机制,对外暴露install方法
// this allows calling useStore() outside of a component setup after
// installing pinia's plugin
setActivePinia(pinia) // 设置当前活跃的 pinia
if (!isVue2) {
pinia._a = app
app.provide(piniaSymbol, pinia) // 通过provide传递pinia实例,提供给后续使用
app.config.globalProperties.$pinia = pinia // 设置全局属性 $pinia
/* istanbul ignore else */
if (__DEV__ && IS_CLIENT) {
// @ts-expect-error: weird type in devtools api
registerPiniaDevtools(app, pinia)
}
toBeInstalled.forEach((plugin) => _p.push(plugin)) // // 加载pinia插件
toBeInstalled = []
}
},
use(plugin) { // pinia对外暴露的插件用法
if (!this._a && !isVue2) {
toBeInstalled.push(plugin) // 将插件存入toBeInstalled,待初始化的时候使用
} else {
_p.push(plugin)
}
return this
},
_p,
// it's actually undefined here
// @ts-expect-error
_a: null,
_e: scope,
_s: new Map<string, StoreGeneric>(),
state, // 所有状态
})
// pinia devtools rely on dev only features so they cannot be forced unless
// the dev build of Vue is used
if (__DEV__ && IS_CLIENT) {
// 集成 vue devtools
pinia.use(devtoolsPlugin)
}
return pinia
}
关键源码分析已在上面标明
2. 定义 defineStore
业务测用法
import { defineStore } from 'pinia'
export const useCounterStore = defineStore('counter', {
state: () => {
return { count: 0 }
},
// could also be defined as
// state: () => ({ count: 0 })
actions: {
increment() {
this.count++
},
},
})
源码实现
路径(packages/pinia/src/store.ts)
可以看到defineStore
最终返回useStore
,并标记唯一$id
根据传参格式,获取id和options
if (typeof idOrOptions === 'string') {
id = idOrOptions
// the option store setup will contain the actual options in this case
options = isSetupStore ? setupOptions : setup
} else {
options = idOrOptions
id = idOrOptions.id
}
useStore
useStore代码逐步分析
const currentInstance = getCurrentInstance()
pinia =
// in test mode, ignore the argument provided as we can always retrieve a
// pinia instance with getActivePinia()
(__TEST__ && activePinia && activePinia._testing ? null : pinia) ||
(currentInstance && inject(piniaSymbol))
通过vue
的getCurrentInstance
拿到当前的vue实例,接着判断pinia
是否存在,不存在的话,通过inject(piniaSymbol)
获取(install时提供的app.provide(piniaSymbol, pinia)
)
if (pinia) setActivePinia(pinia)
设置当前活跃的pinia
实例,有多个pinia
实例时,方便获取当前活跃的pinia
实例
setActivePinia
代码路径(packages/pinia/src/rootStore.ts)
export let activePinia: Pinia | undefined
export const setActivePinia = (pinia: Pinia | undefined) =>
(activePinia = 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.`
)
}
activePinia 不存在时,报错提示入口use(pinia)
if (!pinia._s.has(id)) {
// creating the store registers it in `pinia._s`
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
}
}
一开始,pinia._s.has(id)
没值,所以进入里面的逻辑,
这里传参格式已
defineStore('counter', {
state: () => {
return { count: 0 }
},
// could also be defined as
// state: () => ({ count: 0 })
actions: {
increment() {
this.count++
},
},
})
为例,所以走else的逻辑
else {
createOptionsStore(id, options as any, pinia)
}
接着看createOptionsStore(id, options as any, pinia)
函数做了什么
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]
let store: Store<Id, S, G, A>
function setup() {
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
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])
return assign(
localState,
actions,
Object.keys(getters || {}).reduce((computedGetters, name) => {
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>)
)
}
store = createSetupStore(id, setup, options, pinia, hot)
store.$reset = function $reset() {
const newState = state ? state() : {}
// we use a patch to group all changes into one single subscription
this.$patch(($state) => {
assign($state, newState)
})
}
return store as any
}
根据传参,初始化数据。setup
函数整合state、getters
成响应式数据
,再整合actions
合并返回
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
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])
localState
把业务测传入的state
整成响应式的
Object.keys(getters || {}).reduce((computedGetters, name) => {
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>)
上面代码把getters
的value值由原本的普通函数
转成计算属性,返回回调函数带有store参数
,所以业务测可以获取到state
,如下:
getters: {
double: (state) => state.n * 2,
},
接着调用 store = createSetupStore(id, setup, options, pinia, hot)
分析下createSetupStore
函数里的核心代码
$patch方法
function $patch(
partialStateOrMutator:
| DeepPartial<UnwrapRef<S>>
| ((state: UnwrapRef<S>) => void)
): void {
let subscriptionMutation: SubscriptionCallbackMutation<S>
isListening = false
// reset the debugger events since patches are sync
/* istanbul ignore else */
if (__DEV__) {
debuggerEvents = []
}
if (typeof partialStateOrMutator === 'function') {
partialStateOrMutator(pinia.state.value[$id] as UnwrapRef<S>)
subscriptionMutation = {
type: MutationType.patchFunction,
storeId: $id,
events: debuggerEvents as DebuggerEvent[],
}
} else {
/*
** 合并partialStateOrMutator 到 state中。
** 比如:counter.$patch({ count: counter.count + 1 }),
** { count: counter.count + 1 } 更新到 state中
*/
mergeReactiveObjects(pinia.state.value[$id], partialStateOrMutator)
subscriptionMutation = {
type: MutationType.patchObject,
payload: partialStateOrMutator,
storeId: $id,
events: debuggerEvents as DebuggerEvent[],
}
}
isListening = true
console.log('---subscriptions', subscriptions)
// because we paused the watcher, we need to manually call the subscriptions
triggerSubscriptions(
subscriptions,
subscriptionMutation,
pinia.state.value[$id] as UnwrapRef<S>
)
}
先看下业务测使用方式
// $patch是更新store的一种方式
counter.$patch({ count: counter.count + 1 })
根据传入的partialStateOrMutator
参数类型,走相应的逻辑。以上面例子为例:
走以下逻辑
else {
/*
** 合并partialStateOrMutator 到 state中。
** 比如:counter.$patch({ count: counter.count + 1 }),
** { count: counter.count + 1 } 更新到 state中
*/
mergeReactiveObjects(pinia.state.value[$id], partialStateOrMutator)
...
}
mergeReactiveObjects
函数作用:合并partialStateOrMutator 到 state中
比如:
counter.$patch({ count: counter.count + 1}),
// { count: counter.count + 1 } 更新到 state中
接着定义$dispose
,处理清除逻辑
function $dispose() {
scope.stop()
subscriptions = []
actionSubscriptions = []
pinia._s.delete($id)
}
接着整合partialStore
partialStore
整合_p、$id、$onAction、$patch、$subscribe(callback, options = {})、$dispose
,后续会合并到useStore()
接着整合store
,挂载到pinia._s.set($id, store)
const store: Store<Id, S, G, A> = reactive(
assign(
__DEV__ && IS_CLIENT
? // devtools custom properties
{
_customProperties: markRaw(new Set<string>()),
_hmrPayload,
}
: {},
partialStore
// must be added later
// setupStore
)
) as unknown as Store<Id, S, G, A>
// store the partial store now so the setup of stores can instantiate each other before they are finished without
// creating infinite loops.
pinia._s.set($id, store)
接着调用assign(store, setupStore)
,把setupStore值(state,getters,actions等)值合并进来(因为store是引用类型,所以会改变)
最后,useStore
函数返回如下
const store: StoreGeneric = pinia._s.get(id)!
return store as any
pinia._s.get(id)
值就是上面pinia._s.set($id, store)
设置的
3.总结
Pinia YYDS