Appearance
Pinia
对于 Pinia 其定义一个 Store 是通过 defineStore 的方式去进行的,
defineStore
其提供了两种定义方式
- Option 配置的方式
ts
const store = defineStore("storeId", {
state: () => {
a: 1
},
getters: {},
actions: {
increment() {
this.a++
},
},
})
const store = defineStore({
id: "storeId2",
state: () => {
a: 1
},
getters: {},
actions: {
increment() {
this.a++
},
},
})
- setup 配置的方式
ts
const store = defineStore("storeId3", () => {
const a = ref(1)
const increment = () => {
a.value++
}
return {
a,
increment,
}
})
那么我们就基于这两种定义方式为入口去看看 defineStore 的核心源码
ts
export function defineStore(
// TODO: add proper types from above
idOrOptions: any, // store的 id
setup?: any,
setupOptions?: any
): StoreDefinition {
let id: string
let options:
| DefineStoreOptions<string, StateTree, _GettersTree<StateTree>, _ActionsTree>
| DefineSetupStoreOptions<string, StateTree, _GettersTree<StateTree>, _ActionsTree>
// 1. 处理入参,判断第二个参数是否为函数类型
const isSetupStore = typeof setup === "function"
// 判断第一个参数是否为字符串, 字符串代表就是store的id
if (typeof idOrOptions === "string") {
id = idOrOptions
// the option store setup will contain the actual options in this case
options = isSetupStore ? setupOptions : setup
} else {
// 非字符串类型的那肯定是 Option配置类型的入参
options = idOrOptions
id = idOrOptions.id
}
// 提供核心的useStore 方法
function useStore(pinia?: Pinia | null, hot?: StoreGeneric): StoreGeneric {
//.....
}
useStore.$id = id
return useStore
}
其主要就两个功能
- 处理入参,根据三种入参类型的传入方式去获取入参的 id、 options、setup 和 isSetupStore
- 生成 useStore 方法并返回
useStore
这是提供了 Hooks 的方式让用户去使用 store 的所有功能能力,这样就可以在组件或者其他地方通过 const store = useStore();
从而获取到当前 store 中的 state、getter、action 和其他方法函数。
核心源码
ts
// 提供核心的useStore 方法
function useStore(pinia?: Pinia | null, hot?: StoreGeneric): StoreGeneric {
// 1. 获取全家的 pinia 实例对象
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)
pinia = activePinia!
// 2. 初次访问的时候初始化 store 实例对象
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)
}
}
// 获取当前store实例对象
const store: StoreGeneric = pinia._s.get(id)!
// StoreGeneric cannot be casted towards Store
return store as any
}
分析
主要分为三个步骤
获取当前 app 下的唯一的 pinia 实例对象(非 store 对象)
在初次访问当前 store 的时候,通过两种方式去初始化 store 实例对象,并将其以 store.id 为 key 存储到
pinia._s
中向外提供 store 实例对象
其核心就是两种初始化 store 的方法
- createSetupStore : setup 的方式去初始化 store
- createOptionsStore : option 的方式去初始化 store(其实际上也是组装成 setup 然后通过 setup 的方式去初始化)
createSetupStore
setup 方式创建的 store
例子
ts
const store = defineStore("storeId3", () => {
const a = ref(1)
const increment = () => {
a.value++
}
return {
a,
increment,
}
})
其实简单分析其也继承了很多 Vuex 的特点(state、getter、action),移除了 mutations 的概念,例外其不像 Vuex 使用 this.$store.xxx
方式去执行对应的方法,而是提供了$patch
、$reset
、$subscribe
、$dispose
、$onAction
等方法。下面我们按照处理这些属性或者方法的功能去分析源码
state
在 state 方面其跟 Vuex 不同的地方主要是,其本身是响应式,所以我们可以直接修改其内容就自动触发对应的更新
从结果可以看出对于 state 其存放在两个地方 : 1. store 对象上 2. store.$state 对象上。 这样我们使用 currentDate 的时候就可以有两种方式
- store 对象上
const { currentDate } = store
- store.$state 对象上
const { currentDate } = store.$state
下面我们从源码分析
源码
ts
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> {
let scope!: EffectScope
// state 1. 获取是否存在初始化的值
// 主要提供给将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
// state 2. 如果不存在那就初始化{}
if (!isOptionsStore && !initialState && (!__DEV__ || !hot)) {
/* istanbul ignore if */
if (isVue2) {
set(pinia.state.value, $id, {})
} else {
pinia.state.value[$id] = {}
}
}
const hotState = ref({} as S)
// 3. 按照 state actions getters去初始化一个缓存数据的对象
const _hmrPayload = /*#__PURE__*/ markRaw({
actions: {} as Record<string, any>,
getters: {} as Record<string, Ref>,
state: [] as string[],
hotState,
})
// 4. 响应式 合并生成一个初始化的 store对象
const store: Store<Id, S, G, A> = reactive(
__DEV__ || USE_DEVTOOLS
? 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>
// 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)
const runWithContext = (pinia._a && pinia._a.runWithContext) || fallbackRunWithContext
// TODO: idea create skipSerialize that marks properties as non serializable and they are skipped
// 5. 执行 setup 函数获取 store的结果
const setupStore = runWithContext(() => pinia._e.run(() => (scope = effectScope()).run(setup)!))!
// overwrite existing actions to support $onAction
// 对返回的结果每一个值进行分别处理
// 1. ref 类型 或者 reactive 类型 => 存放到 store.state.value[key] 上
// 2. 计算属性类型 => 存放到 _hmrPayload.getters[key]
// 3. 函数类型 => 存放到 setupStore[key] 和 _hmrPayload.actions[key]
for (const key in setupStore) {
const prop = setupStore[key]
if ((isRef(prop) && !isComputed(prop)) || isReactive(prop)) {
// mark it as a piece of state to be serialized
if (__DEV__ && hot) {
set(hotState.value, key, toRef(setupStore as any, key))
// createOptionStore directly sets the state in pinia.state.value so we
// can just skip that
} else 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
}
}
/* istanbul ignore else */
if (__DEV__) {
_hmrPayload.state.push(key)
}
// action
} 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
}
/* istanbul ignore else */
if (__DEV__) {
_hmrPayload.actions[key] = prop
}
// list actions so they can be used in plugins
// @ts-expect-error
optionsForPlugin.actions[key] = prop
} else if (__DEV__) {
// add getters for devtools
if (isComputed(prop)) {
_hmrPayload.getters[key] = isOptionsStore
? // @ts-expect-error
options.getters[key]
: prop
if (IS_CLIENT) {
const getters: string[] =
(setupStore._getters as string[]) ||
// @ts-expect-error: same
((setupStore._getters = markRaw([])) as string[])
getters.push(key)
}
}
}
}
// add the state, getters, and action properties
/* istanbul ignore if */
// 6. 将setup执行和初次处理的结果合并到 store中
if (isVue2) {
Object.keys(setupStore).forEach(key => {
set(store, key, setupStore[key])
})
} else {
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)
}
// use this instead of a computed with setter to be able to create it anywhere
// without linking the computed lifespan to wherever the store is first
// created.
// 7. 创建store.$state对象
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
$patch($state => {
assign($state, state)
})
},
})
// only apply hydrate to option stores with an initial state in pinia
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
}
分析
- 初始化一些对象, 如
initialState
、hotState
、_hmrPayload
、store
- 执行 store 内容获取结果(执行 setup 的内容获取初始化的值)
- 对于 setupResult 进行分类处理
- ref 类型 或者 reactive 类型 => 存放到
store.state.value[key]
上
- ref 类型 或者 reactive 类型 => 存放到
- 计算属性类型 => 存放到
_hmrPayload.getters[key]
- 计算属性类型 => 存放到
- 函数类型 => 存放到
setupStore[key]
和_hmrPayload.actions[key]
- 函数类型 => 存放到
- 将 setup 执行的结果合并到 store 中
ts
if (isVue2) {
Object.keys(setupStore).forEach(key => {
set(store, key, setupStore[key])
})
} else {
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)
}
这样我们就可以通过 store.currentDate 获取到对应的值了,但是这时候 store.$state 还不存在,那么下一步就是
- 创建 store.$state 对象
ts
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
$patch($state => {
assign($state, state)
})
},
})
对于访问 其代理到了 pinia.state.value[$id]
上 (对于 pinia 其会将所有的 store 的 state 按照 id 存放到 pinia.state.value 上)
对于赋值,很取巧也就是 触发$patch
函数
所以对于 state 的更新其有两种方式
- 直接修改 store.xxx 或者 store.$state.xxx
- 通过
$patch(() => { state.xxxx = xxx})
的方式去修改 - action 中也可以修改
changeCurrentDate = () => {this.xxx = xxx }
getters
从上面 state 我们其实已经整体了解 pinia 对于关键三元素 state、getter、actions 的存储方式,下面直接简单说明一下
在处理 setupResult 结果分类的时候对于 计算类型 的会将其存放到
setupStore._getters
和_hmrPayload.getters[key]
在 assign 的时候将 setupResult 结果合并到 store 中
ts
if (isVue2) {
Object.keys(setupStore).forEach(key => {
set(store, key, setupStore[key])
})
} else {
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)
}
actions
对于 action 类型的 其不会简单的通过 assgin 合并到 store 上了,而是在处理 setupResult 的时候进行了一个 wrapAction 的包装,其包装的源码为
ts
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
// 覆盖原来的action方法
setupStore[key] = actionValue
}
}
下面我们详细看一下 wrapAction
wrapAction
对于 action 的执行 pinia 需要在其执行前、执行成功后、执行失败后执行一些订阅的回调函数,目的主要是为了触发用户自定义的 如 store.$onAction
或者插件定义了一个函数
ts
/**
* Wraps an action to handle subscriptions.
* actions 执行回调的核心方法
*
* @param name - name of the action action的名称
* @param action - action to wrap action的回调函数
* @returns a wrapped action to handle subscriptions
*/
function wrapAction(name: string, action: _Method) {
return function (this: any) {
// 重置pinia实例对象
setActivePinia(pinia)
const args = Array.from(arguments)
// 存放 action执行成功后的回调函数
const afterCallbackList: Array<(resolvedReturn: any) => any> = []
// 存放 action执行失败后的回调函数
const onErrorCallbackList: Array<(error: unknown) => unknown> = []
// 提供 store.$onAction订阅的对于action修改的监听函数中对于 执行成功的回调钩子
function after(callback: _ArrayType<typeof afterCallbackList>) {
afterCallbackList.push(callback)
}
// 提供 store.$onAction订阅的对于action修改的监听函数中对于 执行失败的回调钩子
function onError(callback: _ArrayType<typeof onErrorCallbackList>) {
onErrorCallbackList.push(callback)
}
// @ts-expect-error
// 触发通过 store.$onAction订阅的对于action修改的监听函数
triggerSubscriptions(actionSubscriptions, {
args,
name,
store,
after,
onError,
})
let ret: any
try {
// 回调action
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 => {
// 触发action执行成功的回调钩子函数
triggerSubscriptions(afterCallbackList, value)
return value
})
.catch(error => {
triggerSubscriptions(onErrorCallbackList, error)
return Promise.reject(error)
})
}
// trigger after callbacks
// 触发action执行成功的回调钩子函数
triggerSubscriptions(afterCallbackList, ret)
return ret
}
}
可以通过一个简单的例子去分析
ts
const { $onAction, changeCurrentDate } = useStore()
$onAction(({ args, name, store, after, onError }) => {
// 当changeCurrentDate触发的时候其 name = changeCurrentDate args = xxxx
after(() => {
console.log(name + " 执行完成了") // changeCurrentDate 执行完成了
})
onError(() => {
console.log(name + " 执行失败了") // changeCurrentDate 执行失败了
})
})
执行$onAction 的时候会将这个回到函数存放到 actionSubscriptions 数组中,然后 triggerSubscriptions(actionSubscriptions, { args, name, store, after, onError, })
这一步会触发这个回调函数,并执行 after 和 onError,这样 after 的回调就会 push 到 afterCallbackList 数组中在函数执行完成后执行
$patch
这是 pinia 提供的修改 state 数据的方法,其主要分为两种方式(跟 setState 很像)
- 对象类型入参
$patch({ currentDate : 1})
注意: 这边跟 React.setState 不一样的地方是 这个是和深层遍历合并的方式,所以对于子属性为对象类型的时候不会导致其失去响应式
// 对象类型修改state
// 深度遍历响应式合并
// 元数据 : state = { obj :{ a : 1 , b: 2 } }
// 修改 store.$patch({ obj:{ a : 11 , c : 3 } })
// 结果为: state = { obj :{ a : 11 , b: 2 , c : 3 } }
- 回调函数类型
store.$patch((state) => { state.a = 1 })
(推荐的方式)
源码
ts
/**
* 提供修改store中state方法,两种类型
* 1. 对象类型 传入的为 state store.$patch({ a : 1 , b :2 })
* 2. 函数类型 回调入参为 state store.$patch((state) => { state.a = 1 })
* @param partialStateOrMutator
*/
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") {
// 执行回调函数 传入的为整个当前store的state
partialStateOrMutator(pinia.state.value[$id] as UnwrapRef<S>)
subscriptionMutation = {
type: MutationType.patchFunction,
storeId: $id,
events: debuggerEvents as DebuggerEvent[],
}
} else {
// 对象类型修改state
// 深度遍历响应式合并
// 元数据 : state = { obj :{ a : 1 , b: 2 } }
// 修改 store.$patch({ obj:{ a : 11 , c : 3 } })
// 结果为: state = { obj :{ a : 11 , b: 2 , c : 3 } }
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>)
}
分析
因为
pinia.state.value[$id]
是响应式的,所以可以直接修改就可以了订阅者更新钩子的触发
triggerSubscriptions(subscriptions, subscriptionMutation, pinia.state.value[$id] as UnwrapRef<S>)
这一步就会触发用户通过 store.$subscribe
订阅的数据跟新的钩子函数了
$subscribe
订阅 state 数据跟新的回调方法
源码
ts
// 提供监听state数据修改的回调方法
// options.detached => 布尔值,默认是 false,
// - 正常情况下,当订阅所在的组件被卸载时,订阅将被停止删除,
// - 如果设置detached值为 true 时,即使所在组件被卸载,订阅依然在生效
function $subscribe(callback, options = {}) {
// 将当前回调函数存放到 subscriptions 数组中,并返回移出监听的方法
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
订阅 action 执行的回调方法
源码
ts
$onAction: addSubscription.bind(null, actionSubscriptions),
具体可以看 wrapAction , 其核心逻辑也就在 warapAction 中 通过将订阅的回调函数存放到 afterCallbackList
、onErrorCallbackList
然后在对应的时机执行即可