【pinia源码】系列文章
- 【pinia源码】一、createPinia源码解析
- 【pinia源码】二、defineStore源码解析
- 【pinia源码】三、storeToRefs源码解析
- 【pinia源码】四、mapHelper API源码解析
前言
【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++
}
}
})
const useUserStore = defineStore({
id: 'counter',
state: () => ({
count: 0
}),
actions: {
increment() {
this.count++
}
}
})
const 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 = {}
>(
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 = {}
>(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()
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!
if (!pinia._s.has(id)) {
if (isSetupStore) {
createSetupStore(id, setup, options, pinia)
} else {
createOptionsStore(id, options as any, pinia)
}
if (__DEV__) {
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)
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
}
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,
$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
?
{
_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]
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])
}
}
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') {
const actionValue = __DEV__ && hot ? prop : wrapAction(key, prop)
if (isVue2) {
set(setupStore, key, actionValue)
} else {
setupStore[key] = actionValue
}
if (__DEV__) {
_hmrPayload.actions[key] = prop
}
optionsForPlugin.actions[key] = prop
} else if (__DEV__) {
if (isComputed(prop)) {
_hmrPayload.getters[key] = isOptionsStore
?
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
}
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) => {
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 {
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 = true
isSyncListening = true
return store
接下来看下store 中的几个方法:
$onAction
在每个action 中添加回调函数。回调接收一个对象参数:该对象包含name (action 的key 值)、store (当前store )、after (添加action 执行完之后的回调)、onError (添加action 执行过程中的错误回调)、args (action 的参数)属性。
示例:
let count = 0, successCount = 0, failCount = 0
store.$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
})
store.$patch({
name: 'xxx',
age: 14
})
$patch 源码:
function $patch(
partialStateOrMutator:
| _DeepPartial<UnwrapRef<S>>
| ((state: UnwrapRef<S>) => void)
): void {
let subscriptionMutation: SubscriptionCallbackMutation<S>
isListening = isSyncListening = false
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
triggerSubscriptions(
subscriptions,
subscriptionMutation,
pinia.state.value[$id] as UnwrapRef<S>
)
}
$reset
通过构建一个新的state object 将state 重置为初始状态。只在options 配置下生效。如果是setup 配置,开发环境下报错。
store.$reset = function $reset() {
const newState = state ? 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 = {}) {
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
}
在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 = []
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() {
if (!initialState && (!__DEV__ || !hot)) {
if (isVue2) {
set(pinia.state.value, id, state ? state() : {})
} else {
pinia.state.value[id] = state ? state() : {}
}
}
const localState =
__DEV__ && hot
?
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)
const store = pinia._s.get(id)!
if (isVue2 && !store._r) return
return getters![name].call(store, store)
})
)
return computedGetters
}, {} as Record<string, ComputedRef>)
)
}
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 中会根据传入参数构造一个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
|