前言#
昨天我们看了cretePinia的逻辑,了解了初始化的逻辑。
今天我们来看下defineStore。
defineStore#
切换测试用例#
昨天我们用的测试用例是没有数据的,今天我们换一个:
这是官方提供的:
const useA = defineStore('a', {
state: () => ({ a: 'a' }),
})
it('retrieves the root state of one store', () => {
const pinia = createPinia()
useA(pinia)
expect(pinia.state.value).toEqual({
a: { a: 'a' },
})
})不过我一般的写法是setup stores:
export const useCounterStore = defineStore('counter', () => {
const count = ref(0)
function $reset() {
count.value = 0
}
return { count, $reset }
})所以我这里修改下测试用例
const useC = defineStore('c', () => {
const user = reactive({
name: 'c',
gender: 'male',
age: 18,
})
return {
user,
}
})
it('retrieves the root state of one store', () => {
const pinia = createPinia()
useC(pinia)
expect(pinia.state.value).toEqual({
c: {
user: {
name: 'c',
gender: 'male',
age: 18,
},
},
})
})源码#
前面一大段的函数重载
export function defineStore<
Id extends string,
S extends StateTree = {},
G extends _GettersTree<S> = {},
// cannot extends ActionsTree because we loose the typings
A /* extends ActionsTree */ = {},
>(
id: Id,
options: Omit<DefineStoreOptions<Id, S, G, A>, 'id'>
): StoreDefinition<Id, S, G, A>
/**
* Creates a `useStore` function that retrieves the store instance
*
* @param options - options to define the store
*/
export function defineStore<
Id extends string,
S extends StateTree = {},
G extends _GettersTree<S> = {},
// cannot extends ActionsTree because we loose the typings
A /* extends ActionsTree */ = {},
>(options: DefineStoreOptions<Id, S, G, A>): StoreDefinition<Id, S, G, A>
/**
* Creates a `useStore` function that retrieves the store instance
*
* @param id - id of the store (must be unique)
* @param storeSetup - function that defines the store
* @param options - extra options
*/
export function defineStore<Id extends string, SS>(
id: Id,
storeSetup: () => SS,
options?: DefineSetupStoreOptions<
Id,
_ExtractStateFromSetupStore<SS>,
_ExtractGettersFromSetupStore<SS>,
_ExtractActionsFromSetupStore<SS>
>
): StoreDefinition<
Id,
_ExtractStateFromSetupStore<SS>,
_ExtractGettersFromSetupStore<SS>,
_ExtractActionsFromSetupStore<SS>
>我们来看下核心代码:
export function defineStore(
// TODO: add proper types from above
idOrOptions: any,
setup?: any,
setupOptions?: any
): StoreDefinition {
let id: string
let options:
| DefineStoreOptions<
string,
StateTree,
_GettersTree<StateTree>,
_ActionsTree
>
| DefineSetupStoreOptions<
string,
StateTree,
_GettersTree<StateTree>,
_ActionsTree
>
const isSetupStore = typeof setup === 'function'
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
if (__DEV__ && typeof id !== 'string') {
throw new Error(
`[🍍]: "defineStore()" must be passed a store id as its first argument.`
)
}
}
function useStore(pinia?: Pinia | null, hot?: StoreGeneric): StoreGeneric {
const hasContext = hasInjectionContext()
pinia =
// in test mode, ignore the argument provided as we can always retrieve a
// pinia instance with getActivePinia()
(__TEST__ && activePinia && activePinia._testing ? null : pinia) ||
(hasContext ? inject(piniaSymbol, null) : null)
if (pinia) setActivePinia(pinia)
if (__DEV__ && !activePinia) {
throw new Error(
`[🍍]: "getActivePinia()" was called but there was no active Pinia. Are you trying to use a store before calling "app.use(pinia)"?\n` +
`See https://pinia.vuejs.org/core-concepts/outside-component-usage.html for help.\n` +
`This will fail in production.`
)
}
pinia = activePinia!
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
}
}
const store: StoreGeneric = pinia._s.get(id)!
if (__DEV__ && hot) {
const hotId = '__hot:' + id
const newStore = isSetupStore
? createSetupStore(hotId, setup, options, pinia, true)
: createOptionsStore(hotId, assign({}, options) as any, pinia, true)
hot._hotUpdate(newStore)
// cleanup the state properties and the store from the cache
delete pinia.state.value[hotId]
pinia._s.delete(hotId)
}
if (__DEV__ && IS_CLIENT) {
const currentInstance = getCurrentInstance()
// save stores in instances to access them devtools
if (
currentInstance &&
currentInstance.proxy &&
// avoid adding stores that are just built for hot module replacement
!hot
) {
const vm = currentInstance.proxy
const cache = '_pStores' in vm ? vm._pStores! : (vm._pStores = {})
cache[id] = store
}
}
// StoreGeneric cannot be casted towards Store
return store as any
}
useStore.$id = id
return useStore
}代码量不多,就不做删减了。
-
idOrOptions:我们defineStore的第一个参数,作为唯一标识符。 -
setup:我们传入的第二个参数,我们这里传入了一个函数:() => { const user = __vite_ssr_import_3__.reactive({ name: "c", gender: "male", age: 18 }); return { user }; } -
isSetupStore: 不多说,我们的写法自然就是setup store,注意,只要传入函数就会被认定是setup store的写法。
最后它返回一个useStore的函数,并把我们的唯一标识符赋值给这个函数$id。
而这个useStore的函数就是我们业务代码中引入的hook,我们需要执行这个hook才能拿到最终的数据:
const userStore = useC()这里我们不需要传入pinia作为参数,因为我们有全局activePinia。
然后我们来看下useStore执行的逻辑。
-
hasInjectionContext:这个函数来自于vue(vue-demi):function hasInjectionContext() { return !!(currentInstance || currentRenderingInstance || currentApp); }查看是否可以执行
inject逻辑,前面一章我们说过了currentInstance、currentRenderingInstsance的作用以及provide/inject的实现逻辑,所以这里不多说。 -
pinia = inject(piniaSymbol, null):没啥好说的,从当前组件实例中拿到pinia,前面一章我们知道了install的时候这货会用vue的provide将自己注入所有组件实例中,所以理论上是拿得到的。 -
if (pinia) setActivePinia(pinia):这里是将全局的activePinia设置成当前组件实例绑定的pinia实例。你可呢在疑惑为什么要这么做,install到vue根组件的时候不是已经这么做了吗?为什么还要再设置一次?因为不一定只有一个vue根组件,也不一定只有一个pinia实例。可能存在两个pinia实例,比如:it('does not mix up different applications', () => { const pinia1 = createPinia() const pinia2 = createPinia() useA(pinia1) useB(pinia2) expect(pinia1.state.value).toEqual({ a: { a: 'a' }, }) expect(pinia2.state.value).toEqual({ b: { b: 'b' }, }) }) const app1 = createApp(App1).mount('a').use(pinia1); const app2 = createApp(App2).mount('b').use(pinia2);这就是俩
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)
// auth.js
import { defineStore, acceptHMRUpdate } from 'pinia'
export const useAuth = defineStore('auth', {
// options...
})
// make sure to pass the right store definition, `useAuth` in this case.
if (import.meta.hot) {
import.meta.hot.accept(acceptHMRUpdate(useAuth, import.meta.hot))
}这么做就可以搭vite热更新的车了。
具体如何实现的,因为涉及到vite热更新的原理,我们以后再分析,现阶段先跳过。
createSetupStore#
这货代码较长,我这里就不贴整体的了,我们拆开了一部分一部分分析:
函数整体:
function createSetupStore<
Id extends string,
SS extends Record<any, unknown>,
S extends StateTree,
G extends Record<string, _Method>,
A extends _ActionsTree,
>(
$id: Id,
setup: () => SS,
options:
| DefineSetupStoreOptions<Id, S, G, A>
| DefineStoreOptions<Id, S, G, A> = {},
pinia: Pinia,
hot?: boolean,
isOptionsStore?: boolean
): Store<Id, S, G, A> {
// ...
const store: Store<Id, S, G, A> = reactive(
__DEV__ || (__USE_DEVTOOLS__ && IS_CLIENT)
? assign(
{
_hmrPayload,
_customProperties: markRaw(new Set<string>()), // devtools custom properties
},
partialStore
// must be added later
// setupStore
)
: partialStore
) as unknown as Store<Id, S, G, A>
pinia._s.set($id, store as Store)
// ...
return store
}可以看到最终是返回一个reactive包裹的响应式store数据,这个数据就是最终我们useXX返回的数据,对应前面useStore函数的返回store。
然后我们来看下这个partialStore:
const partialStore = {
_p: pinia,
// _s: scope,
$id,
$onAction: addSubscription.bind(null, actionSubscriptions),
$patch,
$reset,
$subscribe(callback, options = {}) {
const removeSubscription = addSubscription(
subscriptions,
callback,
options.detached,
() => stopWatcher()
)
const stopWatcher = scope.run(() =>
watch(
() => pinia.state.value[$id] as UnwrapRef<S>,
(state) => {
if (options.flush === 'sync' ? isSyncListening : isListening) {
callback(
{
storeId: $id,
type: MutationType.direct,
events: debuggerEvents as DebuggerEvent,
},
state
)
}
},
assign({}, $subscribeOptions, options)
)
)!
return removeSubscription
},
$dispose,
} as _StoreWithState<Id, S, G, A>
_p:这里注意和pinia实例的_p区分开来,pinia实例的_p指向pinia的插件。这里则指向pinia实例。
然后没啥好说的了,这就是在初始化一个store实例。
我们接着看下每个property是干啥用的。
$onAction
首先是犯下傲慢的$onAction:Interface: _StoreWithState | Pinia (vuejs.org)
$onAction: addSubscription.bind(null, actionSubscriptions)
export function addSubscription<T extends _Method>(
subscriptions: T[],
callback: T,
detached?: boolean,
onCleanup: () => void = noop
) {
subscriptions.push(callback)
const removeSubscription = () => {
const idx = subscriptions.indexOf(callback)
if (idx > -1) {
subscriptions.splice(idx, 1)
onCleanup()
}
}
if (!detached && getCurrentScope()) {
onScopeDispose(removeSubscription)
}
return removeSubscription
}就是一个简单的订阅和取消订阅的方法,这里只是订阅,还没涉及到发布。
所以这个$onAction就是用来订阅的,另外根据官方文档的描述:
Setups a callback to be called every time an action is about to get invoked
这货可以用在每次action触发后的回调。
返回一个函数用来手动注销回调。
看下基础用法:
store.$onAction(({ after, onError }) => {
// Here you could share variables between all of the hooks as well as
// setting up watchers and clean them up
after((resolvedValue) => {
// can be used to cleanup side effects
. // `resolvedValue` is the value returned by the action, if it's a
. // Promise, it will be the resolved value instead of the Promise
})
onError((error) => {
// can be used to pass up errors
})
})然后我们再来看下$patch
$patch
Interface: _StoreWithState | Pinia (vuejs.org)
在看代码之前,我们先看下基础用法:
store.$patch({ user })
// or
store.$patch((state) => {
state.user = name
})
然后看下代码
// avoid triggering too many listeners
// https://github.com/vuejs/pinia/issues/1129
let activeListener: Symbol | undefined
function $patch(stateMutation: (state: UnwrapRef<S>) => void): void
function $patch(partialState: _DeepPartial<UnwrapRef<S>>): void
function $patch(
partialStateOrMutator:
| _DeepPartial<UnwrapRef<S>>
| ((state: UnwrapRef<S>) => void)
): void {
let subscriptionMutation: SubscriptionCallbackMutation<S>
isListening = isSyncListening = 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 {
mergeReactiveObjects(pinia.state.value[$id], partialStateOrMutator)
subscriptionMutation = {
type: MutationType.patchObject,
payload: partialStateOrMutator,
storeId: $id,
events: debuggerEvents as DebuggerEvent[],
}
}
const myListenerId = (activeListener = Symbol())
nextTick().then(() => {
if (activeListener === myListenerId) {
isListening = true
}
})
isSyncListening = true
// because we paused the watcher, we need to manually call the subscriptions
triggerSubscriptions(
subscriptions,
subscriptionMutation,
pinia.state.value[$id] as UnwrapRef<S>
)
}代码稍微复杂了点。
首先,如果传入的是函数,那么会将store实例的state传递给我们传入和函数,否则直接合并俩state。。前面一直没有解释类型,因为可读性较差,另外是觉得没必要,现在是时候看下UnwrapRef<T>了:
export type UnwrapRef<T> = T extends ShallowRef<infer V> ? V : T extends Ref<infer V> ? UnwrapRefSimple<V> : UnwrapRefSimple<T>;
export type ShallowRef<T = any> = Ref<T> & {
[ShallowRefMarker]?: true;
};
export interface Ref<T = any> {
value: T;
/**
* Type differentiator only.
* We need this to be in public d.ts but don't want it to show up in IDE
* autocomplete, so we use a private Symbol instead.
*/
[RefSymbol]: true;
}
type UnwrapRefSimple<T> = T extends Builtin | Ref | RefUnwrapBailTypes[keyof RefUnwrapBailTypes] | {
[RawSymbol]?: true;
} ? T : T extends Map<infer K, infer V> ? Map<K, UnwrapRefSimple<V>> & UnwrapRef<Omit<T, keyof Map<any, any>>> : T extends WeakMap<infer K, infer V> ? WeakMap<K, UnwrapRefSimple<V>> & UnwrapRef<Omit<T, keyof WeakMap<any, any>>> : T extends Set<infer V> ? Set<UnwrapRefSimple<V>> & UnwrapRef<Omit<T, keyof Set<any>>> : T extends WeakSet<infer V> ? WeakSet<UnwrapRefSimple<V>> & UnwrapRef<Omit<T, keyof WeakSet<any>>> : T extends ReadonlyArray<any> ? {
[K in keyof T]: UnwrapRefSimple<T[K]>;
} : T extends object & {
[ShallowReactiveMarker]?: never;
} ? {
[P in keyof T]: P extends symbol ? T[P] : UnwrapRef<T[P]>;
} : T;
S extends StateTree,
不是我不想解释,实在是太类型体操了。
简单的说下UnwrapRef<T>的作用,就是用来取消ref包裹数据的那一层。
原本我们需要调用.value才能访问到这个数据,使用UnwrapRef<T>之后就不需要了。
来看下官方的例子:Reactivity Fundamentals | Vue.js (vuejs.org)
const count = ref(0)
const state = reactive({
count
})
console.log(state.count) // 0
state.count = 1
console.log(count.value) // 1当它被赋值给一个reactive的属性的时候它会自动解除外面那一层。
前面我们知道了usexx返回的就是一个reactive的store实例,所以这里state自然就是UnwrapRef。
不过这里你可能会有疑惑,为什么传给我们回调的是pinia.state.value[$id]
这就需要我们回到createPinia那里:
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>>>(() =>
ref<Record<string, StateTree>>({})
)!这里的state就是上面的pinia.state,因为pinia本身是raw的,所以state并不能变成UnwrapRef。
ok,但是这里还有个问题,初始化的时候是空的,这里却是从里面拿出store来,这不对啊。
实际上前面还有逻辑我没给出:
const initialState = pinia.state.value[$id] as UnwrapRef<S> | undefined
// avoid setting the state for option stores if it is set
// by the setup
if (!isOptionsStore && !initialState && (!__DEV__ || !hot)) {
/* istanbul ignore if */
if (isVue2) {
set(pinia.state.value, $id, {})
} else {
pinia.state.value[$id] = {}
}
}
// ...
if (typeof partialStateOrMutator === 'function') {
partialStateOrMutator(pinia.state.value[$id] as UnwrapRef<S>)
subscriptionMutation = {
type: MutationType.patchFunction,
storeId: $id,
events: debuggerEvents as DebuggerEvent[],
}
}
// ...
就是在这里,因为我们传给$patch的是函数,所以我们是自己忘state上面加数据的,所以这里只需要判断是否为空,为空初始化这个实例在state上的数据即可。
那么到这我们就知道了为什么这里是从pinia.state.value[$id]中拿到的数据。
貌似扯的有些远了,我们回到之前的代码,在执行完我们传入的函数之后。
我们前面看的$onAction就是设置回调可以在每次action之后执行回调,那么这里我们在更新数据之后就需要触发回调(pinia只有action)。
则例isListening和isSyncListening俩参数暂时还不清楚是干什么用的,所以先暂时搁置,后面遇到再解释。
triggerSubscriptions:
export function triggerSubscriptions<T extends _Method>(
subscriptions: T[],
...args: Parameters<T>
) {
subscriptions.slice().forEach((callback) => {
callback(...args)
})
}就是简单的执行回调,注意是slice是clone一个,而不是直接占用。
这里args里有俩参数:
if (__DEV__) {
debuggerEvents = []
}
// args第一个参数
subscriptionMutation = {
type: MutationType.patchFunction,
storeId: $id,
events: debuggerEvents as DebuggerEvent[],
}
// args第二个参数
pinia.state.value[$id] as UnwrapRef<S>
注意这里第一个参数里有一个debuggerEvents,只有开发阶段才能使用,我们可以往里面传入debug回调。
相关链接:Interface: SubscriptionCallbackMutationPatchFunction | Pinia (vuejs.org)
到这我们暂时只知道如何存储,还不知道如何调用,等会遇到了我们再说。
ok, $patch分析到这,我们有俩个点还没分析:
- debuggerEvents
- isListening以及isSyncListening
然后我们来看下$reset
$reset
Interface: _StoreWithState | Pinia (vuejs.org)
Resets the store to its initial state by building a new state object.
不多说,直接看代码
const $reset = isOptionsStore
? function $reset(this: _StoreWithState<Id, S, G, A>) {
const { state } = options as DefineStoreOptions<Id, S, G, A>
const newState = state ? state() : {}
// we use a patch to group all changes into one single subscription
this.$patch(($state) => {
assign($state, newState)
})
}
: /* istanbul ignore next */
__DEV__
? () => {
throw new Error(
`🍍: Store "${$id}" is built using the setup syntax and does not implement $reset().`
)
}
: noop可以看到只有使用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.
{
// ...
$subscribe(callback, options = {}) {
const removeSubscription = addSubscription(
subscriptions,
callback,
options.detached,
() => stopWatcher()
)
const stopWatcher = scope.run(() =>
watch(
() => pinia.state.value[$id] as UnwrapRef<S>,
(state) => {
if (options.flush === 'sync' ? isSyncListening : isListening) {
callback(
{
storeId: $id,
type: MutationType.direct,
events: debuggerEvents as DebuggerEvent,
},
state
)
}
},
assign({}, $subscribeOptions, options)
)
)!
return removeSubscription
}
// ...
}- 先将我们的回调放入到订阅器中,不过和前面
$onAction不同的是这里还传入了一个() => stopWatcher()作为addSubscription的第四个参数onCleanup,在执行函数返回的函数即注销回调会执行这个onCleanup。触发stopWatcher stopWatcher中有个scope,我们来看下这货是从哪来的:
// pinia里的
function createSetupStore (...) {
let scope!: EffectScope
// ...
const partialStore = { ... };
const store = reactive(...);
const runWithContext = (pinia._a && pinia._a.runWithContext) || fallbackRunWithContext
// TODO: idea create skipSerialize that marks properties as non serializable and they are skipped
const setupStore = runWithContext(() =>
pinia._e.run(() => (scope = effectScope()).run(setup)!)
)!
}
// vue里的
/**
* Runs a function with the app as active instance. This allows using of `inject()` within the function to get access
* to variables provided via `app.provide()`.
*
* @param fn - function to run with the app as active instance
*/
runWithContext<T>(fn: () => T): T;
const app = {
// ...
runWithContext(fn) {
const lastApp = currentApp;
currentApp = app;
try {
return fn();
} finally {
currentApp = lastApp;
}
}
// ...
}_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里的逻辑,整理下:
let isListening: boolean // set to true at the end
let isSyncListening: boolean // set to true at the end
let activeListener: Symbol | undefined
const partialStore = {
// ...
$patch () {
isListening = isSyncListening = false
// ...
const myListenerId = (activeListener = Symbol())
nextTick().then(() => {
if (activeListener === myListenerId) {
isListening = true
}
})
isSyncListening = true
// ...
}
// ...
$subscribe () {
// ...
const stopWatcher = scope.run(() => watch(
// ...
if (options.flush === 'sync' ? isSyncListening : isListening) {
callback(...)
}
))
// ...
}
}
export interface WatchOptionsBase extends DebuggerOptions {
flush?: 'pre' | 'post' | 'sync';
}这里需要补充watch的知识:Watchers | Vue.js (vuejs.org)
简单的说,就是用来解决大数据量操作时同步触发大量watch回调的问题,所以vue针对这点增加了watch的这一特性,可以让开发者定义什么时候执行watch回调。
默认情况下,watch回调是在父组件更新后子组件DOM更新前触发(flush: sync),这么做是避免watch里改动数据引起重复渲染。
当然,如果你有这方面的想法,想在子组件(当前组件)更新之后再执行,可以设置flush: post:
watch(source, callback, {
flush: 'post'
})
watchEffect(callback, {
flush: 'post'
})
import { watchPostEffect } from 'vue'
watchPostEffect(() => {
/* executed after Vue updates */
})或者说在这之前获取:
watch(source, callback, {
flush: 'sync'
})
watchEffect(callback, {
flush: 'sync'
})
import { watchSyncEffect } from 'vue'
watchSyncEffect(() => {
/* executed synchronously upon reactive data change */
})ok,回到我们的代码中。在知道了watch可以手动调整执行回调timing之后,我们知道了这里nextTick + activeListener === myListenerId的作用,就是用来判断当前是否是flush: post执行,如果是,那么isListening = true。
而$subscribe里的watch只能在flush和post两种情况下执行,记录isListening是确保不会执行错回调,毕竟这个回调只能在这个store实例数据刷新后执行。
ok,还剩最后一个$dispose
$dispose
Interface: _StoreWithState | Pinia (vuejs.org)
function $dispose() {
scope.stop()
subscriptions = []
actionSubscriptions = []
pinia._s.delete($id)
}这货不用多说,就是remove这个store实例。
ok,跳出partialStrore回到我们的createSetupStore函数中
在执行了runWithContext之后我们拿到了setupStore,为了方便分析,我加了点其它的东西

然后我们接着看代码(部分dev和hmr相关的代码省略):
// ...
for (const key in setupStore) {
const prop = setupStore[key]
if ((isRef(prop) && !isComputed(prop)) || isReactive(prop)) {
if (!isOptionsStore) {
// in setup stores we must hydrate the state and sync pinia state tree with the refs the user just created
if (initialState && shouldHydrate(prop)) {
if (isRef(prop)) {
prop.value = initialState[key]
} else {
// probably a reactive object, lets recursively assign
// @ts-expect-error: prop is unknown
mergeReactiveObjects(prop, initialState[key])
}
}
// transfer the ref to the pinia state to keep everything in sync
/* istanbul ignore if */
if (isVue2) {
set(pinia.state.value[$id], key, prop)
} else {
pinia.state.value[$id][key] = prop
}
}
} else if (typeof prop === 'function') {
// @ts-expect-error: we are overriding the function we avoid wrapping if
const actionValue = __DEV__ && hot ? prop : wrapAction(key, prop)
// this a hot module replacement store because the hotUpdate method needs
// to do it with the right context
/* istanbul ignore if */
if (isVue2) {
set(setupStore, key, actionValue)
} else {
// @ts-expect-error
setupStore[key] = actionValue
}
// list actions so they can be used in plugins
// @ts-expect-error
optionsForPlugin.actions[key] = prop
}
// ...
}
// ...
/**
* Returns whether a value should be hydrated
*
* @param obj - target variable
* @returns true if `obj` should be hydrated
*/
export function isPlainObject(
// eslint-disable-next-line @typescript-eslint/no-explicit-any
o: any
): o is StateTree {
return (
o &&
typeof o === 'object' &&
Object.prototype.toString.call(o) === '[object Object]' &&
typeof o.toJSON !== 'function'
)
}
const skipHydrateSymbol = Symbol();
function shouldHydrate(obj: any) {
return isVue2
? /* istanbul ignore next */ !skipHydrateMap.has(obj)
: !isPlainObject(obj) || !obj.hasOwnProperty(skipHydrateSymbol)
}-
shouldHydrate:是否需要注入,如果是对象并且不在store中,那就需要合并 -
mergeReactiveObjects:篇幅问题,这里就不贴代码了,就是在合并响应式数据 -
wrapAction:这货得看下代码:/** * Wraps an action to handle subscriptions. * * @param name - name of the action * @param action - action to wrap * @returns a wrapped action to handle subscriptions */ function wrapAction(name: string, action: _Method) { return function (this: any) { setActivePinia(pinia) const args = Array.from(arguments) const afterCallbackList: Array<(resolvedReturn: any) => any> = [] const onErrorCallbackList: Array<(error: unknown) => unknown> = [] function after(callback: _ArrayType<typeof afterCallbackList>) { afterCallbackList.push(callback) } function onError(callback: _ArrayType<typeof onErrorCallbackList>) { onErrorCallbackList.push(callback) } // @ts-expect-error triggerSubscriptions(actionSubscriptions, { args, name, store, after, onError, }) let ret: unknown try { ret = action.apply(this && this.$id === $id ? this : store, args) // handle sync errors } catch (error) { triggerSubscriptions(onErrorCallbackList, error) throw error } if (ret instanceof Promise) { return ret .then((value) => { triggerSubscriptions(afterCallbackList, value) return value }) .catch((error) => { triggerSubscriptions(onErrorCallbackList, error) return Promise.reject(error) }) } // trigger after callbacks triggerSubscriptions(afterCallbackList, ret) return ret } }它是用于
普通函数的。简单的说就是包裹一层普通函数,让它也能触发action回调以及函数自己的回调。
然后继续回到createSetupStore中,最终
// ...
assign(store, setupStore)
// allows retrieving reactive objects with `storeToRefs()`. Must be called after assigning to the reactive object.
// Make `storeToRefs()` work with `reactive()` #799
assign(toRaw(store), setupStore)
// ...
Object.defineProperty(store, '$state', {
get: () => (__DEV__ && hot ? hotState.value : pinia.state.value[$id]),
set: (state) => {
/* istanbul ignore if */
if (__DEV__ && hot) {
throw new Error('cannot set hotState')
}
$patch(($state) => {
assign($state, state)
})
},
})注意这里的store是前面基于partialStore创建的store,没有数据的,而setupStore则是数据。
接着我们继续往下看:
// apply all plugins
pinia._p.forEach((extender) => {
/* istanbul ignore else */
if (__USE_DEVTOOLS__ && IS_CLIENT) {
const extensions = scope.run(() =>
extender({
store: store as Store,
app: pinia._a,
pinia,
options: optionsForPlugin,
})
)!
Object.keys(extensions || {}).forEach((key) =>
store._customProperties.add(key)
)
assign(store, extensions)
} else {
assign(
store,
scope.run(() =>
extender({
store: store as Store,
app: pinia._a,
pinia,
options: optionsForPlugin,
})
)!
)
}
})这是在触发插件并把结果赋值给store。
最后一部分:
if (__DEV__ && !isVue2) {
$subscribeOptions.onTrigger = (event) => {
/* istanbul ignore else */
if (isListening) {
debuggerEvents = event
// avoid triggering this while the store is being built and the state is being set in pinia
} else if (isListening == false && !store._hotUpdating) {
// let patch send all the events together later
/* istanbul ignore else */
if (Array.isArray(debuggerEvents)) {
debuggerEvents.push(event)
} else {
console.error(
'🍍 debuggerEvents should be an array. This is most likely an internal Pinia bug.'
)
}
}
}
}
isListening = true
isSyncListening = true
return 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
createOptionsStore#
这货就比较简单了,因为格式有板有眼,直接贴代码:
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) => {
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)
// it was created just before
const store = pinia._s.get(id)!
// allow cross using stores
/* istanbul ignore if */
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, true)
return store as any
}这里唯一要说的是computedGetter。
先看下getter的基础用法:
export const useCounterStore = defineStore('counter', {
state: () => ({
count: 0,
}),
getters: {
doubleCount: (state) => state.count * 2,
},
})回归性原理:getters就是computed!
这里还有一点就是markRaw,说明getter数据改动不会引起响应式更新。
其它没啥好说的了。。。
总结#
我是没想到这一章内容这么多。。。。
简单总结下defineStore里做了什么:
- 区分你是
setupStore还是optionsStore - 返回
useStore函数,这个函数就是我们业务代码调用时的const xx = useXX()的useXX。 useStore里首先将上下文调整为符合当前pinia实例的vue根实例- 根据是否是
setupStore分别调用createSetupStore和createOptionsStore - 返回上面俩函数执行拿到的
store实例 - 俩函数的逻辑上面总结了,这里就不总结了。
突然发现没了,我们要分析的内容到这里就结束了~
