前言
项目一直在用 Pinia,虽然没看过它内部实现方式,但是大致能猜到,周末刚好有时间,作为一个源码深度爱好者,我们还是来一探究竟吧~
Pinia 是什么我就不再多介绍了,小伙伴们自己去看官网哦。
相关链接:
开始
我们直接去 Github 拉一份最新的源码(这里用的是 pinia@3.0.3
版本):
git clone -b v3 https://github.com/vuejs/pinia.git
接着我们在根目录安装依赖:
pnpm install || npm install
安装完依赖后,我们进入到官方提供的一个 demo 项目(playground)中,并启动该项目:
cd ./packages/playground/ && pnpm play
分析源码之前先贴一张源码的流程图:
createPinia
我们可以通过该方法获得一个 pinia 实例。
找到 demo 项目的 playground/src/main.ts
入口文件:
import { computed, createApp, markRaw, Ref } from 'vue'
import App from './App.vue'
import { createPinia } from 'pinia'
import { router } from './router'
// 创建一个 pinia 实例
const pinia = createPinia()
// 扩展 pinia 实例
declare module 'pinia' {
export interface PiniaCustomProperties {
set route(
value: RouteLocationNormalizedLoaded | Ref<RouteLocationNormalizedLoaded>
)
get route(): RouteLocationNormalized
}
}
// 自定义插件
pinia.use(() => ({
route: computed(() => markRaw(router.currentRoute.value)),
}))
// 使用 app.use 挂载 pinia 实例
const app = createApp(App)
.use(pinia)
.use(router)
// used in counter setup for tests
.provide('injected', 'global')
app.mount('#app')
可以看到,我们通过 createPinia()
方法获得了一个 pinia 实例,下面我们进入 createPinia
方法源码内部,看它到底做了什么。
找到源码 packages/pinia/src/createPinia.ts
文件的第 10 行:
import { Pinia, PiniaPlugin, piniaSymbol, setActivePinia } from './rootStore'
import { App, effectScope, markRaw, ref, Ref } from 'vue'
import { devtoolsPlugin, registerPiniaDevtools } from './devtools'
import { IS_CLIENT } from './env'
import { StateTree, StoreGeneric } from './types'
/**
* Creates a Pinia instance to be used by the application
*/
export function createPinia(): Pinia {
// 创建一个副作用 scope
const scope = effectScope(true)
// 存放 pinia 内所有 store 的响应式对象
const state = scope.run<Ref<Record<string, StateTree>>>(() =>
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 的插件方法
// 设置当前活跃的 pinia
setActivePinia(pinia)
pinia._a = app
// 提供全局 provide
app.provide(piniaSymbol, pinia)
// 扩展全局属性
app.config.globalProperties.$pinia = pinia
/* istanbul ignore else */
// 安装开发者插件
if (__USE_DEVTOOLS__ && IS_CLIENT) {
registerPiniaDevtools(app, pinia)
}
toBeInstalled.forEach((plugin) => _p.push(plugin))
toBeInstalled = []
},
use(plugin) {
if (!this._a) {
toBeInstalled.push(plugin)
} 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. Avoid old browsers like IE11.
if (__USE_DEVTOOLS__ && IS_CLIENT && typeof Proxy !== 'undefined') {
pinia.use(devtoolsPlugin)
}
return pinia
}
/**
* Dispose a Pinia instance by stopping its effectScope and removing the state, plugins and stores. This is mostly
* useful in tests, with both a testing pinia or a regular pinia and in applications that use multiple pinia instances.
* Once disposed, the pinia instance cannot be used anymore.
*
* @param pinia - pinia instance
*/
export function disposePinia(pinia: Pinia) {
pinia._e.stop()
pinia._s.clear()
pinia._p.splice(0)
pinia.state.value = {}
// @ts-expect-error: non valid
pinia._a = null
}
app.use(pinia)
使用 pinia 的时候,我们需要使用 Vue 的 App 实例的 use()
方法去挂载 pinia
实例:
// 使用 app.use 挂载 pinia 实例
const app = createApp(App)
.use(pinia)
.use(router)
// used in counter setup for tests
.provide('injected', 'global')
app.mount('#app')
Vue 会调用 pinia 实例提供的 install 方法,然后将 app 当参数传入 pinia 的 install 方法:
// ...
export function createPinia(): Pinia {
// 创建一个副作用 scope
const scope = effectScope(true)
// 存放 pinia 内所有 store 的响应式对象
const state = scope.run<Ref<Record<string, StateTree>>>(() =>
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 的插件方法
// 设置当前活跃的 pinia
setActivePinia(pinia)
pinia._a = app
// 提供全局 provide
app.provide(piniaSymbol, pinia)
// 扩展全局属性
app.config.globalProperties.$pinia = pinia
/* istanbul ignore else */
// 安装开发者插件
if (__USE_DEVTOOLS__ && IS_CLIENT) {
registerPiniaDevtools(app, pinia)
}
toBeInstalled.forEach((plugin) => _p.push(plugin))
toBeInstalled = []
},
// ...
}
return pinia
}
//...
install 方法
可以看到, install 方法主要做了以下操作:
-
调用
setActivePinia
方法设置当前活跃的 pinia 实例。packages/pinia/src/rootStore.ts
文件的第 34 行:export let activePinia: Pinia | undefined // 设置当前活跃的 pinia 实例 export const setActivePinia: _SetActivePinia = (pinia) => (activePinia = pinia) // 获取当前活跃的 pinia 实例 export const getActivePinia = () => (hasInjectionContext() && inject(piniaSymbol)) || activePinia
-
调用
app.provide
方法设置全局 provide,返回 pinia 实例。 -
扩展 Vue 的 app 实例,将 pinia 实例挂载到 $pinia 属性。
-
开发环境安装开发者插件。
所以在项目中,我们通过以下方式获取 pinia 实例:
import { getCurrentInstance } from 'vue'
import { getActivePinia } from 'pinia'
// 方式一:常用方式
const pinia1 = getActivePinia()
// 方式二:组件内可以直接用 $pinia 属性访问
const currInstance = getCurrentInstance()
const pinia2 = currInstance.proxy.$pinia
console.log(pinia1 === pinia2) // true
小伙伴们自己灵活运用就行。
defineStore 方法
通过该方法我们可以定义一个 Store,它会返回一个创建特定 store 实例的方法 useStore
。
我们可以找到 demo 项目的 packages/playground/src/stores/counter.ts
文件:
import { acceptHMRUpdate, defineStore } from 'pinia'
const delay = (t: number) => new Promise((r) => setTimeout(r, t))
export const useCounter = defineStore('counter', {
state: () => ({
n: 2,
incrementedTimes: 0,
decrementedTimes: 0,
numbers: [] as number[],
}),
getters: {
double: (state) => state.n * 2,
},
actions: {
increment(amount = 1) {
if (typeof amount !== 'number') {
amount = 1
}
this.incrementedTimes++
this.n += amount
},
changeMe() {
console.log('change me to test HMR')
},
async fail() {
const n = this.n
await delay(1000)
this.numbers.push(n)
await delay(1000)
if (this.n !== n) {
throw new Error('Someone changed n!')
}
return n
},
async decrementToZero(interval: number = 300, usePatch = true) {
if (this.n <= 0) return
while (this.n > 0) {
if (usePatch) {
this.$patch({
n: this.n - 1,
decrementedTimes: this.decrementedTimes + 1,
})
} else {
this.n -= 1
}
await delay(interval)
}
},
},
})
if (import.meta.hot) {
import.meta.hot.accept(acceptHMRUpdate(useCounter, import.meta.hot))
}
可以看到,调用了 Pinia 提供的 defineStore()
方法定义了一个 useCounter
变量。
接下来我们进入到 defineStore()
方法源码。
找到 packages/pinia/src/store.ts
文件的第 838 行:
// ...
export function defineStore(
// TODO: add proper types from above
id: any,
setup?: any,
setupOptions?: any
): StoreDefinition {
let options:
| DefineStoreOptions<
string,
StateTree,
_GettersTree<StateTree>,
_ActionsTree
>
| DefineSetupStoreOptions<
string,
StateTree,
_GettersTree<StateTree>,
_ActionsTree
>
// 判断创建方式是 setup 还是 options
const isSetupStore = typeof setup === 'function'
// the option store setup will contain the actual options in this case
options = isSetupStore ? setupOptions : setup
// 构建 useStore 方法
function useStore(pinia?: Pinia | null, hot?: StoreGeneric): StoreGeneric {
// ...
}
useStore.$id = id
// 返回 useStore 方法
return useStore
}
// ...
从源码可以看出,定义 Store 的方式有两种:
- setup 方式:以函数方式定义。
- options 方式:以对象形式定义。
前面的 packages/playground/src/stores/counter.ts
文件中用的就是 options 方式定义的 Store。
换成 setup 方式为 packages/playground/src/stores/counterSetup.ts
文件:
import { computed, toRefs, reactive, inject } from 'vue'
import { acceptHMRUpdate, defineStore } from 'pinia'
const delay = (t: number) => new Promise((r) => setTimeout(r, t))
export const useCounter = defineStore('counter-setup', () => {
const state = reactive({
n: 0,
incrementedTimes: 0,
decrementedTimes: 0,
numbers: [] as number[],
})
const injected = inject('injected', 'fallback value')
console.log('injected (should be global)', injected)
const double = computed(() => state.n * 2)
function increment(amount = 1) {
if (typeof amount !== 'number') {
amount = 1
}
state.incrementedTimes++
state.n += amount
}
function changeMe() {
console.log('change me to test HMR')
}
async function fail() {
const n = state.n
await delay(1000)
state.numbers.push(n)
await delay(1000)
if (state.n !== n) {
throw new Error('Someone changed n!')
}
return n
}
async function decrementToZero(interval: number = 300) {
if (state.n <= 0) return
while (state.n > 0) {
state.n -= 1
state.decrementedTimes += 1
await delay(interval)
}
}
return {
...toRefs(state),
double,
increment,
fail,
changeMe,
decrementToZero,
}
})
if (import.meta.hot) {
import.meta.hot.accept(acceptHMRUpdate(useCounter, import.meta.hot))
}
我们先不介绍两者有啥区别,我们继续源码往下看。
useStore()方法
可以使用 useStore()
方法获取一个特定 Store 的实例。
我们找到 demo 项目的 packages/playground/src/views/AllStores.vue
文件:
<template>
<p>
I have a store "{{ userStore.name }}". I have
{{ cartStore.items.length }} items in the cart.
</p>
<div>
<p>Counter: {{ counterStore.double }} = 2 x {{ counterStore.n }}</p>
<button @click="counterStore.increment(10)">Increment</button>
<button @click="counterStore.fail()">Fail</button>
<button @click="counterStore.decrementToZero(300)">Countdown!</button>
</div>
</template>
<script setup lang="ts">
import { useUserStore } from '../stores/user'
import { useCartStore } from '../stores/cart'
import { useCounter } from '../stores/counter'
// 获取 User 的 Store 实例
const userStore = useUserStore()
// 获取 Cart 的 Store 实例
const cartStore = useCartStore()
// 获取 Counter 的 Store 实例
const counterStore = useCounter()
</script>
可以看到,当调用了 useUserStore()
、useCartStore()
、useCounter()
方法后,最后都会来到 useStore()
方法。
// ...
export function defineStore(
// TODO: add proper types from above
id: any,
setup?: any,
setupOptions?: any
): StoreDefinition {
let options:
| DefineStoreOptions<
string,
StateTree,
_GettersTree<StateTree>,
_ActionsTree
>
| DefineSetupStoreOptions<
string,
StateTree,
_GettersTree<StateTree>,
_ActionsTree
>
// 判断创建方式是 setup 还是 options
const isSetupStore = typeof setup === 'function'
// the option store setup will contain the actual options in this case
options = isSetupStore ? setupOptions : setup
// 构建 useStore 方法
function useStore(pinia?: Pinia | null, hot?: StoreGeneric): StoreGeneric {
// Vue 是否创建了 App 实例
const hasContext = hasInjectionContext()
// 获取 pinia 实例
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)
// 设置当前活跃的 pinia 对象
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 对象
pinia = activePinia!
// 判断 pinia 中是否已经定义过该 store 实例
if (!pinia._s.has(id)) {
// creating the store registers it in `pinia._s`
if (isSetupStore) {
// 创建 setup 方式的 store 实例
createSetupStore(id, setup, options, pinia)
} else {
// 创建 options 方式的 store 实例
createOptionsStore(id, options as any, pinia)
}
/* istanbul ignore else */
if (__DEV__) {
// @ts-expect-error: not the right inferred type
useStore._pinia = pinia
}
}
// 获取当前 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) {
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
}
}
// 返回创建的 store 对象
return store as any
}
useStore.$id = id
return useStore
}
// ...
可以看到,最主要就是判断 Store 实例创建方式,根据创建方式不同调用不同的方法,当是以 setup 方式创建 Store 实例的时候源码做了啥呢?
当为 setup 创建方式的时候,会调用 createSetupStore()
方法。
createSetupStore() 方法
找到 packages/pinia/src/store.ts
文件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: (helpers: SetupStoreHelpers) => 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
// 提供给自定义插件的参数
const optionsForPlugin: DefineStoreOptionsInPlugin<Id, S, G, A> = assign(
{ actions: {} as A },
options
)
// watcher options for $subscribe
const $subscribeOptions: WatchOptions = { deep: true }
// internal state
let isListening: boolean // set to true at the end
let isSyncListening: boolean // set to true at the end
let subscriptions: SubscriptionCallback<S>[] = []
let actionSubscriptions: StoreOnActionListener<Id, S, G, A>[] = []
let debuggerEvents: DebuggerEvent[] | DebuggerEvent
// 获取 store 初始化 state 对象
const initialState = pinia.state.value[$id] as UnwrapRef<S> | undefined
// 如果是 setup 方式,就初始化一个空的对象
if (!isOptionsStore && !initialState && (!__DEV__ || !hot)) {
/* istanbul ignore if */
pinia.state.value[$id] = {}
}
const hotState = ref({} as S)
// 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
// patch 方法用于对比并修改 store 实例的 state 对象
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 = []
}
// 函数形式修改 state 对象
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
// 触发 state 修改的观察器
triggerSubscriptions(
subscriptions,
subscriptionMutation,
pinia.state.value[$id] as UnwrapRef<S>
)
}
// options 方式的 Store 才提供 store 实例的 $reset 方法
const $reset = isOptionsStore
? function $reset(this: _StoreWithState<Id, S, G, A>) {
const { state } = options as DefineStoreOptions<Id, S, G, A>
const newState: _DeepPartial<UnwrapRef<S>> = state ? state() : {}
// we use a patch to group all changes into one single subscription
this.$patch(($state) => {
// @ts-expect-error: FIXME: shouldn't error?
assign($state, newState)
})
}
: /* istanbul ignore next */
__DEV__
? () => {
throw new Error(
`🍍: Store "${$id}" is built using the setup syntax and does not implement $reset().`
)
}
: noop
// 销毁当前 store 实例
function $dispose() {
scope.stop()
subscriptions = []
actionSubscriptions = []
pinia._s.delete($id)
}
/**
* 封窗 Store 的 action 方法,以便于出发点 $onAction 方法
* @param fn - action to wrap
* @param name - name of the action
*/
const action = <Fn extends _Method>(fn: Fn, name: string = ''): Fn => {
if (ACTION_MARKER in fn) {
// we ensure the name is set from the returned function
;(fn as unknown as MarkedAction<Fn>)[ACTION_NAME] = name
return fn
}
const wrappedAction = 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: wrappedAction[ACTION_NAME],
store,
after,
onError,
})
let ret: unknown
try {
// 触发 action 方法,修改上下文 this 为当前 store 实例,并传入 action 的原始参数
ret = fn.apply(this && this.$id === $id ? this : store, args)
} catch (error) {
// 触发 action 的 error 钩子
triggerSubscriptions(onErrorCallbackList, error)
throw error
}
// 如果 action 返回的 Promise 异步对象
if (ret instanceof Promise) {
return ret
.then((value) => {
// 触发 action 的 callback 钩子
triggerSubscriptions(afterCallbackList, value)
return value
})
.catch((error) => {
// 触发 action 的 error 钩子
triggerSubscriptions(onErrorCallbackList, error)
return Promise.reject(error)
})
}
// 如果 action 返回的不是 Promise 异步对象
// 触发 action 的 callback 钩子
triggerSubscriptions(afterCallbackList, ret)
return ret
} as MarkedAction<Fn>
wrappedAction[ACTION_MARKER] = true
wrappedAction[ACTION_NAME] = name // will be set later
return wrappedAction
}
const _hmrPayload = /*#__PURE__*/ markRaw({
actions: {} as Record<string, any>,
getters: {} as Record<string, Ref>,
state: [] as string[],
hotState,
})
// 初始化 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>
// 将 store 对象变成响应式对象
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>
// 将当前 store 实例存放至 pinia 实例中
pinia._s.set($id, store as Store)
const runWithContext =
(pinia._a && pinia._a.runWithContext) || fallbackRunWithContext
// 调用传入的 setup 函数,获取当前 store 的 actions 以及 state
const setupStore = runWithContext(() =>
pinia._e.run(() => (scope = effectScope()).run(() => setup({ action }))!)
)!
// 遍历 setup 函数返回的对象的所有属性
for (const key in setupStore) {
const prop = setupStore[key]
// 是 Ref 类型 && 非 Computed 的属性 || 是 Reactive 的属性
if ((isRef(prop) && !isComputed(prop)) || isReactive(prop)) {
// mark it as a piece of state to be serialized
if (__DEV__ && hot) {
//...
} 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 as keyof UnwrapRef<S>]
} else {
// probably a reactive object, lets recursively assign
// @ts-expect-error: prop is unknown
mergeReactiveObjects(prop, initialState[key])
}
}
// 替换 state 中对应的属性
pinia.state.value[$id][key] = prop
}
// 封装 action 方法
} else if (typeof prop === 'function') {
const actionValue = __DEV__ && hot ? prop : action(prop as _Method, key)
setupStore[key] = actionValue
optionsForPlugin.actions[key] = prop
} else if (__DEV__) {
// ...
}
}
// 将 setup 方法返回的对象合并到 store 中
assign(store, setupStore)
// 为了兼容 storeToRefs 方法返回的属性
assign(toRaw(store), setupStore)
// 定义 store 的 $state 属性,方便直接修改 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 方法为了触发 state 修改的监听器
$patch(($state) => {
// @ts-expect-error: FIXME: shouldn't error?
assign($state, state)
})
},
})
// 触发所有的自定义插件
pinia._p.forEach((extender) => {
/* istanbul ignore else */
if (__USE_DEVTOOLS__ && IS_CLIENT) {
// ...
} else {
assign(
store,
scope.run(() =>
extender({
store: store as Store,
app: pinia._a,
pinia,
options: optionsForPlugin,
})
)!
)
}
})
// ...
return store
}
虽然 createSetupStore()
方法看起来很多,但是核心就干了这些事情:
- 初始化 store 对象。
- 将当前 store 实例存放至 pinia 实例中。
- 调用传入的 setup 函数,获取当前 store 的 actions 以及 state,遍历 setup 函数返回的对象的所有属性并合并到 store 中,封装传入的 actions 方法,用于触发 action 的监听。
- 定义 store 的 $state 属性,方便直接修改 state
- 创建
$patch
方法:用于对比更新 state,并且触发监听回掉。 - 创建
$reset
方法:用于重置 state。 - 创建
$dispose
方法:用于销毁当前 store 实例。
上面代码都有注释,这里就不再详细介绍啦~
createOptionsStore() 方法
创建 options 的 Store 实例。
packages/pinia/src/store.ts
:
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 */
pinia.state.value[id] = state ? state() : {}
}
// 调用 state 方法,获取 state 对象
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,
// 遍历所有的 getters并封装为 computed
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
// @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>
)
)
}
// 调用 createSetupStore 方法创建 store 实例
store = createSetupStore(id, setup, options, pinia, hot, true)
return store as any
}
从源码可以看出,其实 createOptionsStore()
方法最后也是调用了createSetupStore()
方法创建 store 实例 。
这里最后再补充一下 Pinia 库的热更新模块源码。
HMR (热更新)
Pinia 支持热更新,所以你可以编辑你的 store,并直接在你的应用中与它们互动,而不需要重新加载页面,允许你保持当前的 state、并添加甚至删除 state、action 和 getter。
那么它具体是怎么做的呢?
首先得需要你在对应的 Store 中注册下模块的热更新,比如 packages/playground/src/stores/counter.ts
:
import { acceptHMRUpdate, defineStore } from 'pinia'
export const useCounter = defineStore('counter', {
//...
actions: {
// ...
changeMe() {
console.log('change me to test HMR')
},
//...
})
// 注册该模块的热更新
if (import.meta.hot) {
import.meta.hot.accept(acceptHMRUpdate(useCounter, import.meta.hot))
}
当我们在开发环境的时候,只有打开了模块的更新后,我们才可以做到不需要重新加载页面,允许你保持当前的 state、并添加甚至删除 state、action 和 getter。
那么 acceptHMRUpdate()
方法到底做了什么呢?
packages/pinia/src/hmr.ts
:
// ...
export function acceptHMRUpdate<
Id extends string = string,
S extends StateTree = StateTree,
G extends _GettersTree<S> = _GettersTree<S>,
A = _ActionsTree,
>(initialUseStore: StoreDefinition<Id, S, G, A>, hot: any) {
// strip as much as possible from iife.prod
if (!__DEV__) {
return () => {}
}
return (newModule: any) => {
const pinia: Pinia | undefined = hot.data.pinia || initialUseStore._pinia
if (!pinia) {
// this store is still not used
return
}
// preserve the pinia instance across loads
hot.data.pinia = pinia
// 获取当前更新模块的 exportName
for (const exportName in newModule) {
const useStore = newModule[exportName]
// 判断是否是 Store 模块
if (isUseStore(useStore) && pinia._s.has(useStore.$id)) {
// console.log('Accepting update for', useStore.$id)
const id = useStore.$id
if (id !== initialUseStore.$id) {
console.warn(
`The id of the store changed from "${initialUseStore.$id}" to "${id}". Reloading.`
)
// return import.meta.hot.invalidate()
return hot.invalidate()
}
// 获取热更新前的 store 对象
const existingStore: StoreGeneric = pinia._s.get(id)!
if (!existingStore) {
console.log(`[Pinia]: skipping hmr because store doesn't exist yet`)
return
}
// 调用 useStore 并传入之前已存在的 store 对象,进行属性合并操作。
useStore(pinia, existingStore)
}
}
}
}
并不建议小伙伴们给每个 Store 都默认加上热更新,给那些经常变动的 Store 加上就可以了,毕竟加上了热更新后会对象合并等一系列操作,项目如果有很多 Store 的话,还是会消耗浏览器性能影响开发体验的。
总结
Pinia 的核心实现要点:
- 全局管理:通过
createPinia
创建全局状态容器 - Store 创建:
- Options API 最终转换为 Composition API
- 统一使用
createSetupStore
创建 store
- 响应式处理:
- 使用
reactive
创建响应式 store - 状态通过
ref
/reactive
管理
- 使用
- 插件系统:提供统一的插件扩展点
- 热更新:支持开发时模块热替换
Pinia 的设计简洁高效,既保留了 Vuex 的核心概念,又充分利用了 Composition API 的优势,是 Vue 状态管理的现代解决方案。
ok,源码解析就到这里了,Pinia 的更多用法小伙伴们可以自行查询官网或者研究源码去探索一下哦~