前言
【pinia源码】系列文章次要剖析pinia
的实现原理。该系列文章源码参考pinia v2.0.14
。
源码地址:https://github.com/vuejs/pinia
官网文档:https://pinia.vuejs.org
本篇文章将剖析defineStore
的实现。
应用
通过defineStore
定义一个store
。
const useUserStore = defineStore('counter', { state: () => ({ count: 0 }), actions: { increment() { this.count++ } }})// orconst useUserStore = defineStore({ id: 'counter', state: () => ({ count: 0 }), actions: { increment() { this.count++ } }})// orconst useUserStore = defineStore('counter', () => { const count = ref(0) function increment() { count.value++ } return { count, increment }})
defineStore
export function defineStore( 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 options = isSetupStore ? setupOptions : setup } else { options = idOrOptions id = idOrOptions.id } function useStore(pinia?: Pinia | null, hot?: StoreGeneric): StoreGeneric { // ... } useStore.$id = id return useStore}
defineStore
函数能够接管三个参数:idOrOptions
、setup
、setOptions
,后两个参数为可选参数。上面是三个defineStore
的函数类型定义。
export function defineStore< Id extends string, S extends StateTree = {}, G extends _GettersTree<S> = {}, A /* extends ActionsTree */ = {}>( id: Id, options: Omit<DefineStoreOptions<Id, S, G, A>, 'id'>): StoreDefinition<Id, S, G, A>export function defineStore< Id extends string, S extends StateTree = {}, G extends _GettersTree<S> = {}, A /* extends ActionsTree */ = {} >(options: DefineStoreOptions<Id, S, G, A>): StoreDefinition<Id, S, G, A>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> >
首先在defineStore
中申明了三个变量:id
、options
、isSetupStore
,其中id
为定义的store
的惟一id
,options
为定义store
时的options
,isSetupStore
代表传入的setup
是不是个函数。
而后依据传入的idOrOptions
的类型,为id
、otions
赋值。紧接着申明了一个useStore
函数,并将id
赋给它,而后将其return
。截止到此,咱们晓得defineStore
会返回一个函数,那么这个函数具体是做什么的呢?咱们持续看useStore
的实现。
useStore
function useStore(pinia?: Pinia | null, hot?: StoreGeneric): StoreGeneric { // 获取以后实例 const currentInstance = getCurrentInstance() // 测试环境下,疏忽提供的参数,因为总是能应用getActivePinia()获取pinia实例 // 非测试环境下,如果未传入pinia,则会从组件中应用inject获取pinia pinia = (__TEST__ && activePinia && activePinia._testing ? null : pinia) || (currentInstance && inject(piniaSymbol)) // 设置激活的pinia if (pinia) setActivePinia(pinia) // 如果没有activePinia,那么可能没有install 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为激活的pinia pinia = activePinia! // 从pina._s中查找id否注册过,如果没有被注册,创立一个store并注册在pinia._s中 if (!pinia._s.has(id)) { if (isSetupStore) { createSetupStore(id, setup, options, pinia) } else { createOptionsStore(id, options as any, pinia) } if (__DEV__) { useStore._pinia = pinia } } // 从pinia._s中获取id对应的store 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 && currentInstance && currentInstance.proxy && !hot ) { const vm = currentInstance.proxy const cache = '_pStores' in vm ? vm._pStores! : (vm._pStores = {}) cache[id] = store } // 返回store return store as any}
useStore
接管两个可选参数:pinia
、hot
。pinia
是个Pinia
的实例,而hot
只在开发环境下有用,它与模块的热更新无关。
在useStore
中会首先获取以后组件实例,如果存在组件实例,应用inject(piniaSymbol)
获取pinia
(在install
中会进行provide
),并将其设置为activePinia
,而后在activePinia._s
中查找是否有被注册为id
的store
,如果没有则创立store
,将其注册到activePinia._s
中。最初返回activePinia._s
中id
对应的store
。
当初咱们晓得useStore
函数,最终会返回一个store
。那么这个store
是什么呢?它是如何创立的呢?在useStore
中依据不同状况中有两中形式来创立store
,别离是:createSetupStore
、createOptionsStore
。这两个形式的应用条件是:如果defineStore
第二个参数是个function
调用createSetupStore
,相同调用createOptionsStore
。
createSetupStore
createSetupStore
函数代码过长,这里就不贴残缺代码了。createSetupStore
可接管参数如下:
参数 | 阐明 | |
---|---|---|
$id | 定义store 的id | |
setup | 一个能够返回state 的函数 | |
options | defineStore 的options | |
pinia | Pinia 实例 | |
hot | 是否启用热更新 | 可选 |
isOptionsStore | 是否应用options 申明的store | 可选 |
createSetupStore
代码有500多行,如果从头开始看的话,不容易了解。咱们能够依据createSetupStore
的用处,从其外围开始看。因为createSetupStore
是须要创立store
,并将store
注册到pinia._s
中,所以createSetupStore
中可能须要创立store
,咱们找到创立store
的中央。
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>if (isVue2) { partialStore._r = false}const store: Store<Id, S, G, A> = reactive( assign( __DEV__ && IS_CLIENT ? // devtools custom properties { _customProperties: markRaw(new Set<string>()), _hmrPayload, } : {}, partialStore )) as unknown as Store<Id, S, G, A>pinia._s.set($id, store)
store
是用reactive
包装的一个响应式对象,reactive
所包装的对象是由partialStore
通过Object.assign
进行复制的。partialStore
中定义了很多办法,这些办法都是裸露给用户操作store
的一些接口,如$onAction
可设置actions
的回调、$patch
可更新store
中的state
、$dispose
可销毁store
。
在调用完pinia._s.set($id, store)
之后,会执行setup
,获取所有的数据。setup
的执行会在创立pinia
实例时创立的effectScope
中运行,而且会再独自创立一个effectScope
,用来独自执行setup
.
const setupStore = pinia._e.run(() => { scope = effectScope() return scope.run(() => setup())})!
而后遍历setupStore
的属性:如果prop
(key
对应的值)为ref
(不为computed
)或reactive
,则将key
及prop
同步到pina.state.value[$id]
中;如果prop
为function
,则会应用wrapAction
包装prop
,并将包装后的办法赋值给setupStore[key]
,以笼罩之前的值,同时将包装后的办法存入optionsForPlugin.actions
中。
for (const key in setupStore) { const prop = setupStore[key] // 如果prop是ref(但不是computed)或reactive if ((isRef(prop) && !isComputed(prop)) || isReactive(prop)) { if (__DEV__ && hot) { set(hotState.value, key, toRef(setupStore as any, key)) } else if (!isOptionsStore) { if (initialState && shouldHydrate(prop)) { if (isRef(prop)) { prop.value = initialState[key] } else { mergeReactiveObjects(prop, initialState[key]) } } // 将对应属性同步至pinia.state中 if (isVue2) { set(pinia.state.value[$id], key, prop) } else { pinia.state.value[$id][key] = prop } } if (__DEV__) { _hmrPayload.state.push(key) } } else if (typeof prop === 'function') { // 如果prop是function // 应用wrapAction包装prop,在wrapAction会解决afeterCallback、errorCallback const actionValue = __DEV__ && hot ? prop : wrapAction(key, prop) // 将actionsValue增加到setupStore中,笼罩原来的function if (isVue2) { set(setupStore, key, actionValue) } else { setupStore[key] = actionValue } if (__DEV__) { _hmrPayload.actions[key] = prop } // 将function类型的prop存入optionsForPlugin.actions中 optionsForPlugin.actions[key] = prop } else if (__DEV__) { if (isComputed(prop)) { _hmrPayload.getters[key] = isOptionsStore ? // @ts-expect-error options.getters[key] : prop if (IS_CLIENT) { const getters: string[] = setupStore._getters || (setupStore._getters = markRaw([])) getters.push(key) } } }}
接下来咱们看下wrapAction
是如何进行包装function
类型上的prop
。
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) } triggerSubscriptions(actionSubscriptions, { args, name, store, after, onError, }) let ret: any try { ret = action.apply(this && this.$id === $id ? this : store, args) } catch (error) { triggerSubscriptions(onErrorCallbackList, error) throw error } // 如果后果是promise,在promise中触发afterCallbackList及onErrorCallbackList if (ret instanceof Promise) { return ret .then((value) => { triggerSubscriptions(afterCallbackList, value) return value }) .catch((error) => { triggerSubscriptions(onErrorCallbackList, error) return Promise.reject(error) }) } triggerSubscriptions(afterCallbackList, ret) return ret }}
wrapAction
首先返回一个函数,在这个函数中,首先将pinia
设置为activePinia
,触发actionSubscriptions
中的函数,而后执行action
函数,如果执行过程中出错,会执行onErrorCallbackList
中的errorCallback
,如果没有出错的话,执行afterCallbackList
中的afterCallback
,最初将action
的返回后果return
。
wrapAction
中的actionSubscriptions
是个什么呢?
其实actionSubscriptions
中的callback
就是是通过store.$onAction
增加的回调函数;在执行actionSubscriptions
中的callback
过程中,会将对应callback
增加到afterCallbackList
或onErrorCallbackList
中。例如:
store.$onAction(({ after, onError, name, store }) => { after((value) => { console.log(value) }) onError((error) => { console.log(error) })})
遍历完setupStore
之后,会将setupStore
合并至store
和store
的原始对对象中,以方便使用storeToRefs()
检索响应式对象。
if (isVue2) { Object.keys(setupStore).forEach((key) => { set( store, key, setupStore[key] ) })} else { assign(store, setupStore) assign(toRaw(store), setupStore)}
紧接着拦挡store.$state
的get
、set
办法:当调用store.$state
时,可能从pinia.state.value
找到对应的state
;当应用store.$state = xxx
去批改值时,则调用$patch
办法批改值。
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
就筹备结束。如果在Vue2
环境下,会将store._r
设置为true。
if (isVue2) { store._r = true}
接下来就须要调用应用use
办法注册的plugins
:
pinia._p.forEach((extender) => { if (__DEV__ && IS_CLIENT) { const extensions = scope.run(() => extender({ store, app: pinia._a, pinia, options: optionsForPlugin, }) )! Object.keys(extensions || {}).forEach((key) => store._customProperties.add(key) ) assign(store, extensions) } else { // 将plugin的后果合并到store中 assign( store, scope.run(() => extender({ store, app: pinia._a, pinia, options: optionsForPlugin, }) )! ) }})
最初返回store
。
if ( initialState && isOptionsStore && (options as DefineStoreOptions<Id, S, G, A>).hydrate) { ;(options as DefineStoreOptions<Id, S, G, A>).hydrate!( store.$state, initialState )}isListening = trueisSyncListening = truereturn store
接下来看下store
中的几个办法:
$onAction
在每个action
中增加回调函数。回调接管一个对象参数:该对象蕴含name
(action
的key
值)、store
(以后store
)、after
(增加action
执行完之后的回调)、onError
(增加action
执行过程中的谬误回调)、args
(action
的参数)属性。
示例:
// 统计add action的调用次数let count = 0, successCount = 0, failCount = 0store.$onAction(({ name, after, onError }) => { if (name === 'add') { count++ after((resolveValue) => { successCount++ console.log(resolveValue) }) onError((error) => { failCount++ console.log(error) }) }})
$onAction
外部通过公布订阅模式实现。在pinia
中有个专门的订阅模块subscriptions.ts
,其中蕴含两个次要办法:addSubscription
(增加订阅)、triggerSubscriptions
(触发订阅)。
addSubscription
可接管四个参数:subscriptions
(订阅列表)、callback
(增加的订阅函数)、detached
(游离的订阅,如果为false
在组件卸载后,主动移除订阅;如果为true
,不会主动移除订阅)、onCleanup
(订阅被移除时的回调)
triggerSubscriptions
接管两个参数:subscriptions
(订阅列表)、args
(action
的参数列表)
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 && getCurrentInstance()) { onUnmounted(removeSubscription) } return removeSubscription}export function triggerSubscriptions<T extends _Method>( subscriptions: T[], ...args: Parameters<T>) { subscriptions.slice().forEach((callback) => { callback(...args) })}
$onAction
通过addSubscription.bind(null, actionSubscriptions)
实现。
如何触发订阅?
首先在store
的初始化过程中,会将action
应用wrapAction
函数进行包装,wrapAction
返回一个函数,在这个函数中会先触发actionSubscriptions
,这个触发过程中会将afterCallback
、onErrorCallback
增加到对应列表。而后调用action
,如果调用过程中出错,则触发onErrorCallbackList
,否则触发afterCallbackList
。如果action
的后果是Promise
的话,则在then
中触发onErrorCallbackList
,在catch
中触发onErrorCallbackList
。而后会将包装后的action
笼罩原始action
,这样每次调用action
时就是调用的包装后的action
。
$patch
应用$patch
能够更新state
的值,可进行批量更新。$patch
接管一个partialStateOrMutator
参数,它能够是个对象也能够是个办法。
示例:
store.$patch((state) => { state.name = 'xxx' state.age = 14})// orstore.$patch({ name: 'xxx', age: 14})
$patch
源码:
function $patch( partialStateOrMutator: | _DeepPartial<UnwrapRef<S>> | ((state: UnwrapRef<S>) => void)): void { // 合并的相干信息 let subscriptionMutation: SubscriptionCallbackMutation<S> // 是否触发状态批改后的回调,isListening代表异步触发,isSyncListening代表同步触发 // 此处先敞开回调的触发,避免批改state的过程中频繁触发回调 isListening = isSyncListening = false if (__DEV__) { debuggerEvents = [] } // 如果partialStateOrMutator是个function,执行办法,传入以后的store if (typeof partialStateOrMutator === 'function') { partialStateOrMutator(pinia.state.value[$id] as UnwrapRef<S>) subscriptionMutation = { type: MutationType.patchFunction, storeId: $id, events: debuggerEvents as DebuggerEvent[], } } else { // 如果不是function,则调用mergeReactiveObjects合并state mergeReactiveObjects(pinia.state.value[$id], partialStateOrMutator) subscriptionMutation = { type: MutationType.patchObject, payload: partialStateOrMutator, storeId: $id, events: debuggerEvents as DebuggerEvent[], } } // 当合并完之后,将isListening、isSyncListening设置为true,意味着能够触发状态扭转后的回调函数了 const myListenerId = (activeListener = Symbol()) nextTick().then(() => { if (activeListener === myListenerId) { isListening = true } }) isSyncListening = true // 因为在批改pinia.state.value[$id]的过程中敞开(isSyncListening与isListening)了监听,所以须要手动触发订阅列表 triggerSubscriptions( subscriptions, subscriptionMutation, pinia.state.value[$id] as UnwrapRef<S> )}
$reset
通过构建一个新的state object
将state
重置为初始状态。只在options
配置下失效。如果是setup
配置,开发环境下报错。
store.$reset = function $reset() { // 从新执行state,获取一个新的state const newState = state ? state() : {} // 通过$patch,应用assign将newState合并到$state中 this.$patch(($state) => { assign($state, newState) })}
$subscribe
设置state
扭转后的回调,返回一个移除回调的函数。可承受两个参数:callback
(增加的回调函数)、options:{detached, flush, ...watchOptions}
(detached
同addSubscription
中的detached
;flush
代表是否同步触发回调,可取值:sync
)。
示例:
store.$subribe((mutation: {storeId, type, events}, state) => { console.log(storeId) console.log(type) console.log(state)}, { detached: true, flush: 'sync' })
$subscribe
源码:
function $subscribe(callback, options = {}) { // 将callback增加到subscriptions中,以便应用$patch更新状态时,触发回调 // 当应用removeSubscription移除callback时,进行对pinia.state.value[$id]监听 const removeSubscription = addSubscription( subscriptions, callback, options.detached, () => stopWatcher() ) const stopWatcher = scope.run(() => // 监听pinia.state.value[$id],以触发callback,当应用$patch更新state时,不会进入触发这里的callback 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}
在callback
中的第一个参数中有个type
属性,示意是通过什么形式更新的state
,它有三个值:
MutationType.direct
:通过state.name='xxx'
/store.$state.name='xxx'
等形式批改MutationType.patchObject
:通过store.$patch({ name: 'xxx' })
形式批改MutationType.patchFunction
:通过store.$patch((state) => state.name='xxx')
形式批改
$dispose
销毁store
。
function $dispose() { // 进行监听 scope.stop() // 清空subscriptions及actionSubscriptions subscriptions = [] actionSubscriptions = [] // 从pinia._s中删除store pinia._s.delete($id)}
createOptionsStore
createOptionsStore
可接管参数如下:
参数 | 阐明 | |
---|---|---|
id | 定义store 的id | |
options | defineStore 的options | |
pinia | Pinia 实例 | |
hot | 是否启用热更新 | 可选 |
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() { // 如果pinia.state.value[id]不存在,进行初始化 if (!initialState && (!__DEV__ || !hot)) { if (isVue2) { set(pinia.state.value, id, state ? state() : {}) } else { pinia.state.value[id] = state ? state() : {} } } // 将pinia.state.value[id]各属性值转为响应式对象 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]) // 解决getters,并将解决后的getters和actions合并到localState中 return assign( localState, actions, Object.keys(getters || {}).reduce((computedGetters, name) => { 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>) ) } // 利用createSetupStore创立store store = createSetupStore(id, setup, options, pinia, hot, true) // 重写store.$reset store.$reset = function $reset() { const newState = state ? state() : {} this.$patch(($state) => { assign($state, newState) }) } return store as any}
在createOptionsStore
中会依据传入参数结构一个setup
函数,而后通过createSetupStore
创立一个store
,并重写store.$reset
办法,最初返回store
。
这个setup
函数中会将state()
的返回值赋值给pinia.state.value[id]
,而后将pinia.state.value[id]
进行toRefs
,失去localState
,最初将解决后的getters
和actions
都合并到localState
中,将其返回。对于getters
的解决:将每个getter
函数都转成一个计算属性。
总结
defineStore
返回一个useStore
函数,通过执行useStore
能够获取对应的store
。调用useStore
时咱们并没有传入id
,为什么能精确获取store
呢?这是因为useStore
是个闭包,在执行useStore
执行过程中会主动获取id
。
获取store
的过程:
- 首先获取组件实例
- 应用
inject(piniaSymbol)
获取pinia
实例 - 判断
pinia._s
中是否有对应id
的键,如果有间接取对应的值作为store
,如果没有则创立store
store
创立流程分两种:setup
形式与options
形式
setup
形式:
- 首先在
pinia.state.value
中增加键为$id
的空对象,以便后续赋值 - 应用
reactive
申明一个响应式对象store
- 将
store
存至pinia._s
中 - 执行
setup
获取返回值setupStore
- 遍历
setupStore
的键值,如果值是ref
(不是computed
)或reactive
,将键值增加到pinia.state.value[$id]
中;如果值时function
,首先将值应用wrapAction
包装,而后用包装后的function
替换setupStore
中对应的值 - 将
setupStore
合并到store
中 - 拦挡
store.$state
,使get
操作能够正确获取pinia.state.value[$id]
,set
操作应用this.$patch
更新 - 调用
pinia._p
中的扩大函数,扩大store
options
形式:
- 从
options
中提取state
、getter
、actions
- 构建
setup
函数,在setup
函数中会将getter
解决成计算属性 - 应用
setup
形式创立store
- 重写
store.$reset