pinia当前版本: 2.1.4
pinia是 Vue 的专属状态管理库,并且可以同时支持 Vue 2 和 Vue 3。
在Vue3的项目中,我们都会优先使用Pinia,所以了解其基本的底层原理,有助于我们在项目中更好的应用。
1.createPinia
在Vue3项目中使用pinia时,都会从pinia中引入一个createPinia方法,然后使用这个方法创建一个pinia实例。
import { createPinia } from 'pinia'
// 创建pinia
const pinia = createPinia()
接下来我们就从import { createPinia } from 'pinia'这句代码开始pinia源码的学习。
首先查看createPinia的源码:
// packages/pinia/src/createPinia.ts
/**
* Creates a Pinia instance to be used by the application
*/
export function createPinia(): Pinia {
const scope = effectScope(true)
// 初始化响应式state:默认为一个ref数据【目的是存储:所有store的state的集合】
// RefImpl:{ value: proxy:{} }
// 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>>({})
)!
// 插件列表
let _p: Pinia['_p'] = []
// 待安装插件列表
// plugins added before calling app.use(pinia)
let toBeInstalled: PiniaPlugin[] = []
// 创建pinia实例对象
// 这里用markRaw方法标记了Pinia对象,不需要被转换为响应式数据
const pinia: Pinia = markRaw({
// 定义的Install方法:每个vue插件都需要显示定义,在app.use时调用插件的install方法
install(app: App) {
// this allows calling useStore() outside of a component setup after
// installing pinia's plugin
// 设置为当前活跃的pinia
setActivePinia(pinia)
// 在Vue3应用的情况下:将pinia实例对象挂载到app上
if (!isVue2) {
// 存储Vue3 app应用实例
pinia._a = app
// app.provide()提供一个值,可以在应用中的所有后代组件中注入使用【重点】
app.provide(piniaSymbol, pinia)
// 设置全局属性$pinia:在vue应用内的每个组件都可以通过this.$pinia访问
app.config.globalProperties.$pinia = pinia
// 注册开发者工具
/* istanbul ignore else */
if (USE_DEVTOOLS) {
registerPiniaDevtools(app, pinia)
}
// 将插件列表添加到_p属性
toBeInstalled.forEach((plugin) => _p.push(plugin))
// 清空待安装插件列表
toBeInstalled = []
}
},
// pinia对外暴露的插件注册方法
use(plugin) {
if (!this._a && !isVue2) {
// vue3: 添加到待安装插件列表:初始化时使用
toBeInstalled.push(plugin)
} else {
// vue2:直接添加到_p属性
_p.push(plugin)
}
return this
},
_p, // 插件列表
// it's actually undefined here
// @ts-expect-error
_a: null,// vue3 app应用实例
_e: scope, // effectScope
_s: new Map<string, StoreGeneric>(),// 一个map结构,存储storeId,与store实例的映射关系
state, // 所有的state的集合:每个store初始化时,都会将处理后state数据挂载到Pinia.state.value[$id]下面
})
// 注册开发者工具插件
// 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 && typeof Proxy !== 'undefined') {
pinia.use(devtoolsPlugin)
}
// 返回pinia实例对象
return pinia
}
可以看出createPinia方法的内容比较简单,主要就是创建了一个Pinia实例并返回。pinia实例里面定义了两个方法和几个属性,
我们首先看两个方法:
- install方法:将pinia注册到vue实例。
- use方法:将其他插件注册到pinia实例。
const app = createApp(App)
const pinia = createPinia()
// 注册pinia:会在内部调用pinia的install方法
app.use(pinia)
install方法内容并不多,最主要就是将pinia实例注入到全局,让vue应用下的每个组件实例都可以使用。
app.provide(piniaSymbol, pinia)
然后重点介绍两个属性:
-
pinia._s:可以看出这个属性是一个map结构,它的作用就是存储我们在项目中定义的store实例,它是以键值对的方式进行注册pinia._s.set($id, store),它的作用是在我们多次引用useStore时,不会重复新建Store,而是直接返回已存在的对象。 -
pinia.state:这个属性被初始化为一个ref数据,它的作用是存储每个store实例中的state数据,每个store在创建时,都会将state进行处理后挂载到Pinia.state.value[$id]下面,具体的行为可以在后面createXXXStore中查看。
2.defineStore
下面我们开始阅读defineStore的源码:
// packages/pinia/src/store.ts
/**
* 两个参数:
* 参数1,store的唯一标识符, 传值为string或者{id:string}
* 参数2,可接收两类值,Setup函数或者Options对象;【相当于使用组合式和选项式,随便用哪种都行】
* 参数3,可选,Options对象, 基本没有用到过
*/
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
>
// 根据第二参数的类型来判断是否使用的setupStore模式
const isSetupStore = typeof setup === 'function'
if (typeof idOrOptions === 'string') {
id = idOrOptions
// the option store setup will contain the actual options in this case
// 这里如果为setupStore,setupOptions就是undefined,即options为undefined
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 {
// ...
}
useStore.$id = id
return useStore
}
defineStore方法比较重要,是我们在定义Store时要经常使用的,defineStore方法只有两个参数:
- 参数1:store实例的id,必须保证独一无二。
- 参数2:可接收两类值,Setup函数或者Options对象。
这里defineStore方法里面大部分代码都是useStore函数的内容,所以我们先把它折叠起来,后面再分析。
注意:我们在阅读开源项目源码的时候,一定要学会内容拆分,因为有的函数源码量非常大,有的能达到几百行甚至上千行【比如Vue3源码baseCreateRenderer基础渲染器函数有两千多行代码】,我们要拆分其内容,降低复杂度,然后才能更好的解析其主要逻辑。在熟悉其主要逻辑之后,我们可以进行源码调试,通过step步入每个函数内容,验证之前的逻辑分析。
当我们把useStore函数折叠后,defineStore方法的内容就很简洁明了了,首先处理传入的参数,然后定义了一个useStore方法并且返回,这里的重点是定义的useStore方法:
// 在项目中
const useMainStore = defineStore('main', {...})
这里我们在项目中声明的useMainStore函数内容:就是调用defineStore后返回的useStore方法。
下面我们继续解析useStore源码:
useStore
// store方法
function useStore(pinia?: Pinia | null, hot?: StoreGeneric): StoreGeneric {
// 是否在子组件下使用
const hasContext = hasInjectionContext()
// 获取当前的pinia实例, 如果是开发模式下(activePinia._testing), 则使用inject获取pinia实例, 否则考虑直接使用传入的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)
if (pinia) setActivePinia(pinia)
if (__DEV__ && !activePinia) {
throw new Error(
`[🍍]: "getActivePinia()" was called but there was 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!
// pinia的_s属性为一个map结构,存储键值对:键为storeId,值为store实例
// 从map结构中查询该storeId,如果不存在,则执行创建逻辑
if (!pinia._s.has(id)) {
// creating the store registers it in `pinia._s`
// 根据之前的isSetupStore变量值,划分为两种模式创建逻辑
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)
}
// 处理非ssr模式下, 将store实例存储到vm中, 供devTools使用
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方法一进来调用了inject()来获取当前的pinia实例,这里拿到pinia实例是为了把即将创建的Store实例注册到Pinia._s属性中,以及将state中的数据都挂载到Pinia.state.value[$id]下面。Pinia._s属性是一个map结构,在最前面的createPinia方法有展示过【它的key是storeID,它的值是store实例】。
注意: 这里能够通过
inject(piniaSymbol, null)获取到pinia,是因为在Install方法里面app.provide()已经提供了。同时我们应该知道每一个Symbol()都是唯一的,即Symbol() !== Symbol(),所以我们可以根据piniaSymbol可以获取到正确的pinia。
这里根据storeId来查询Pinia._s属性值是否存在对应的store实例:
- 存在:则取出并直接返回store实例。
- 不存在:则根据之前
isSetupStore变量的值来决定创建store实例的逻辑。
综上所述: useStore方法主要作用就是返回Store实例供我们使用【无则创建,有则直接返回,避免重复创建】。
下面我们开始分析创建store的过程。
有两种定义Store的模式,对应着两种创建Store的方法:
// 1,OptionsStore模式
export const useCounterStore = defineStore('counter', {
state: () => {
return { count: 0 }
},
actions: {
increment() {
this.count++
},
},
})
// 2,SetupStore模式
export const useCounterStore = defineStore('counter', () => {
const count = ref(0)
function increment() {
count.value++
}
return { count, increment }
})
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> {
// 从options对象中取出选项参数
const { state, actions, getters } = options
// 获取初始state:根据storeId获取Store中的初始state数据
const initialState: StateTree | undefined = pinia.state.value[id]
// 创建store变量
let store: Store<Id, S, G, A>
// 定义setup方法【重要】
function setup() {
// 如果不存在初始化数据,则执行state()
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
// 重点,转换成ref数据
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])
// Object.assign() 合并成setupStore原始对象
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}".`
)
}
// getter, 这里markRaw加上computed组合怎么运行? 需要看对应源码
computedGetters[name] = markRaw(
computed(() => {
setActivePinia(pinia)
// it was created just before
const store = pinia._s.get(id)!
// allow cross using stores
/* istanbul ignore next */
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>)
)
}
// 调用createSetupStore创建store实例
store = createSetupStore(id, setup, options, pinia, hot, true)
return store as any
}
可以看见createOptionsStore从options选项对象中取出所需要的数据,然后定义了一个setup函数来处理相关的数据【初始化Store的方法】, 在setup函数中会将store设置到pinia.state.value[id]中,最后调用了createSetupStore这个方法来创建了一个Store实例,并且返回这个实例。
综上所述: 当我们使用OptionsStore模式来定义Store时,createOptionsStore方法内部会将我们传入数据包装成一个setup函数,最终内部还是调用了createSetupStore这个方法来创建的Store实例,即将OptionsStore转换成了SetupStore类型,所以createSetupStore函数才是Pinia中创建Store的真正核心,下面我们继续深入createSetupStore方法源码。
createSetupStore
createSetupStore方法的源码非常多,有五百多行,这里我们就不展示完整代码了,只贴一些关键的逻辑代码:
// 创建SetupStore
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> {
...
// 获取初始state:根据storeId获取Store中的初始state数据
const initialState = pinia.state.value[$id] as UnwrapRef<S> | undefined
// 专门处理setupStore模式
if (!isOptionsStore && !initialState && (!__DEV__ || !hot)) {
if (isVue2) {
set(pinia.state.value, $id, {})
} else {
// 初始化为一个空对象
pinia.state.value[$id] = {}
}
}
...
// 定义了一些方法
// 在partialStore中使用$patch方法
function $patch() {}
// 非setup模式下, 定义$reset方法, 调用$reset()方法可以将 state 重置为初始值, 在plugions中会使用
function $reset() {} // 调用$reset()方法可以将 state 重置为初始值
// 在partialStore中定义$dispose方法, 停止并且删除所有的订阅, 在plugins使用
function $dispose() {}
// 包裹一个action方法, 用来触发错误或者完成回调
function wrapAction() {}
...
// 定义了一个基础store:并且在它的实例上挂载了一些方法, 这里的所有函数都是用于plugins中使用
const partialStore = {
_p: pinia,
// _s: scope,
$id,
$onAction: addSubscription.bind(null, actionSubscriptions),
$patch, // 修改state
$reset, // 重置state
$subscribe() {}, // 监听
$dispose,
} as _StoreWithState<Id, S, G, A>
...
// 使用partialStore为原对象: 创建一个响应式的Store实例
const store = reactive(partialStore)
// 将store添加到pinia._s的map结构中【重点】
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
// 在单独一个scope中执行setup方法, 并获取返回值, 生成原始setupStore对象
const setupStore = pinia._e.run(() => {
scope = effectScope()
return runWithContext(() => scope.run(setup))
})!
// 遍历 setupStore 属性,对其进行处理转换【重点是针对setup模式下的state数据挂载】
// options模式会在setup函数中挂载到state中
for (const key in setupStore) {
const prop = setupStore[key]
// 如果是响应式数据并且不是计算属性, 此时需要手动将initialState数据赋值给响应式数据
if ((isRef(prop) && !isComputed(prop)) || isReactive(prop)) {
// mark it as a piece of state to be serialized
if (!isOptionsStore) {
// in setup stores we must hydrate the state and sync pinia state tree with the refs the user just created
// 如果存在初始state数据,则将初始state数据赋值给响应式数据(通常在热更新时存在)
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 {
// 赋值到state中, options模式会在setup函数中挂载到state中
pinia.state.value[$id][key] = prop
}
}
// action
} else if (typeof prop === 'function') {
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 {
// 更新为wrapAction
setupStore[key] = actionValue
}
// list actions so they can be used in plugins
optionsForPlugin.actions[key] = prop
} else if (__DEV__) {
// 如果是dev模式下加到devtools中
// 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)
}
}
}
}
// 将setupStore中的属性添加到store中
// add the state, getters, and action properties
/* istanbul ignore if */
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)
}
// 定义$state访问器属性,可以通过 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(($state) => {
assign($state, state)
})
},
})
# 返回store实例对象
return store
}
createSetupStore方法里面的内容很多,下面我们贴上一些重点的逻辑逐个解析。
a.挂载初始state数据对象:
// 获取初始state:根据storeId获取Store中的初始state数据
const initialState = pinia.state.value[$id] | undefined
# 专门处理setupStore模式
if (!isOptionsStore && !initialState && (!__DEV__ || !hot)) {
if (isVue2) {
set(pinia.state.value, $id, {})
} else {
// 初始化为一个空对象
pinia.state.value[$id] = {}
}
}
其实上面这段逻辑在createOptionsStore方法中也存在,而这里是专门处理setupStore模式定义store用的,所以这里新增了一个变量isOptionsStore来做区分。
这段代码主要的作用就是在pinia.state.value属性下挂载一个空的state数据对象,key为传入的StoreID。
setupStore模式下:state直接被初始化为一个空对象,因为刚开始它并不知道state会有什么内容,需要经过setup()调用,初始化里面的ref/reactive等响应式数据,处理之后才会挂载到这个空对象里面。
而OptionsStore模式下:直接调用state函数,即可得到初始的state数据对象。
# 调用state()
pinia.state.value[id] = state ? state() : {}
b.$reset方法在option模式下重置state对象
# OptionsStore模式下,重写了$reset方法,
const $reset = isOptionsStore ? function $reset(){} : noop
在OptionsStore模式下:我们可以通过store.$reset()重置state数据对象。
而$reset方法的内容其实也很简单:
# $reset方法
function $reset(this: _StoreWithState<Id, S, G, A>) {
// 取出state方法
const { state } = options as DefineStoreOptions<Id, S, G, A>
# 调用state方法:获取初始的state对象
const newState = state ? state() : {}
// 利用$patch方法修改state数据,
this.$patch(($state) => {
// Object.assign:用原始数据覆盖现在的数据
assign($state, newState)
})
}
在setupStore模式下:
无法通过$reset重置, 因为这个模式下state的初始状态是通过调用setup函数得到的。如果我们调用$reset在开发模式会给出以下警告提示,而在生产模式下不会造成任何副作用。
Store "${$id}" is built using the setup syntax and does not implement $reset()
c.存储已经创建的store实例:
# 将store添加到pinia._s的map结构中【重点】
pinia._s.set($id, store)
在之前的useStore函数中,我们有通过pinia._s.has(id)来判断是否已存在目标Store,如果存在则直接返回Store对象。这里用一个map结构来存储已经创建好的Store实例,避免重复新建Store,因为我们可能会在多个组件中引用一个Store对象。
// 多次引用不会重复新建Store
const mainStore = useMainStore();
在Vue3的源码中也存在这样的逻辑处理,使用一个全局的map结构来存储项目中的target与Proxy对象。
d.对setupStore对象的处理:
- 首先是原始store对象的生成:
// setupStore是最原始的store对象
const runWithContext =
(pinia._a && pinia._a.runWithContext) || fallbackRunWithContext
// TODO: idea create skipSerialize that marks properties as non serializable and they are skipped
// 在单独一个scope中执行setup方法, 并获取返回值, 生成原始setupStore对象
const setupStore = pinia._e.run(() => {
scope = effectScope()
return runWithContext(() => scope.run(setup))
})!
setupStore模式下setup函数内容比较简单:
export const useCounterStore = defineStore('counter', () => {
const count = ref(0)
function increment() {
count.value++
}
// 暴露的数据和方法
return { count, increment }
})
调用结果就是return返回的对象。
而OptionsStore模式下setup内容比较复杂,需要额外的处理:
// OptionsStore的setup初始化过程
// 定义setup方法【重要】
function setup() {
// 如果不存在初始化数据,则执行state()
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
// 重点,转换成ref数据, 如果是热更新则重新生成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])
// Object.assign() 合并成setupStore原始对象
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}".`
)
}
// getter, 并且使用markRaw的方式将生成的computed标记为不作为响应式对象使用
// 使用storeToRefs后可以作为响应式数据
computedGetters[name] = markRaw(
computed(() => {
setActivePinia(pinia)
// it was created just before
const store = pinia._s.get(id)!
// allow cross using stores
/* istanbul ignore next */
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>)
)
}
OptionsStore的setup方法内容比较多,主要就是为了处理state数据【将state中的内容全部转换为ref数据】和getters,最后使用对象的Object.assign()方法将state、actions、getters处理后的内容合并到一个新对象中,并且返回。
所以OptionsStore的setup方法调用后的返回值:就是我们需要的原始store对象即setupStore。
- 循环处理原始setupStore对象中的数据。
// 得到setupStore后, 遍历 setupStore 属性,对其进行处理转换【重点是针对setup模式下的state数据挂载】, 使用在store.$state中
// options模式会在setup函数中挂载到state中
for (const key in setupStore) {
const prop = setupStore[key]
// 如果是响应式数据并且不是计算属性, 此时需要手动将initialState数据赋值给响应式数据
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
// 如果存在初始state数据,则将初始state数据赋值给响应式数据(通常在热更新时存在)
if (initialState && shouldHydrate(prop)) {
if (isRef(prop)) {
prop.value = initialState[key]
} else {
// probably a reactive object, lets recursively assign
mergeReactiveObjects(prop, initialState[key])
}
}
// transfer the ref to the pinia state to keep everything in sync
if (isVue2) {
set(pinia.state.value[$id], key, prop)
} else {
// 赋值到state中, options模式会在setup函数中挂载到state中
pinia.state.value[$id][key] = prop
}
}
// action
} else if (typeof prop === 'function') {
const actionValue = wrapAction(key, prop)
// this a hot module replacement store because the hotUpdate method needs
// to do it with the right context
if (isVue2) {
set(setupStore, key, actionValue)
} else {
// 更新为包裹后的action方法
setupStore[key] = actionValue
}
}
}
- 最后挂载到store实例上:
# 将setupStore中的属性和方法挂载到新建的Store实例上
if (isVue2) {
// vue2:通过set方法来新增响应式数据属性
Object.keys(setupStore).forEach((key) => {
set(store, key, setupStore[key])
})
} else {
// vue3:将处理后setupStore中的内容挂载到Store实例上【state数据和actions中的方法】
assign(store, setupStore)
}
因为这里的store实例是前面新建的响应式数据对象,只存在最基础的属性和方法 【基础版Store】 。
const store = reactive(partialStore)
所以这里需要将我们真正的store中的内容 【即state数据和actions中的方法】 挂载到新建的这个Store实例上,这样这个Store实例才算真正的创建完成,包含了我们所需要的属性和方法。
(五)最后我们再看看 $state属性:
# 定义了一个访问器属性$state
Object.defineProperty(store, '$state', {
// 访问代理
get: () => pinia.state.value[$id],
// 非直接修改的setter,而是在内部使用了$patch方法
set: (state) => {
$patch(($state) => {
# 覆盖key,而非整个替换
assign($state, state)
})
}
})
- getter:可以通过
store.$store访问state数据,getter内部设置了对应的访问代理。 - setter:这里setter并不是直接去修改store的state,而是内部通过patch来安全的修改:
需要预防出现下面这样的操作:你不能完全替换掉 store 的 state,因为那样会破坏其响应性。
store.$state = { count: 24 }
所以setter内部是通过$patch方法来修改state的,这样即使出现上面这样的代码也不会破坏state的响应性。
3.总结
再总结一下两种模式定义Store的创建过程:
optionsStore模式
-
从
options对象取出state、getters、actions。 -
定义
setup函数。 -
重写
$reset方法。 -
创建
partialStore基础store对象,将$patch、$reset、$subscribe等实例方法挂载到该对象上。 -
使用
reactive(partialStore)方法创建响应式Store对象。 -
将创建好的store对象以键值对方式
($id, store)注册到Pinia._s属性中。 -
调用
setup()函数:设置pinia.state.value[id] = state(),然后将localState每个属性设置为ObjectRefImpl数据,将getters中的每个getter转换成计算属性,最后将处理完成后的state、getters、actions合并到一个对象中并返回,生成setupStore。 -
循环
setupStore对象,optionsStore模式在这里仅仅是包装一下actions中的方法。 -
将
setupStore的内容合并到Store对象。 -
给Store对象定义一个
$state访问器属性。 -
返回Store对象。
setupStore模式
-
初始化state,设置
pinia.state.value[$id] = {}空对象。 -
创建
partialStore基础store对象,将$patch、$reset、$subscribe等实例方法挂载到该对象上。 -
使用
reactive(partialStore)方法创建响应式Store对象。 -
将创建好的store对象以键值对方式
($id, store)注册到Pinia._s属性中。 -
调用
setup()函数,初始化内部的ref/computed/reactive响应式数据,获取return返回的setupStore。 -
循环
setupStore对象,循环设置pinia.state.value[$id][key] = prop,同时包装一下actions中的方法。 -
将
setupStore的内容合并到Store对象。 -
给Store对象定义一个
$state访问器属性。 -
返回Store对象。
我们再打印查看一下,创建完成的Store实例的内容:
export const useMainStore = defineStore('main', {
state: () => {
return {
count: 0
}
},
actions: {
addCount() {
this.count++
}
}
})
const mainStore = useMainStore()
console.log(mainStore)
查看Store实例的内容:
4.扩展
最后我们再扩展一下,日常我们直接修改state和通过$patch方法修改state的原理区别。
还是按两种模式展示:
// options模式
const mainStore = useMainStore()
// setup模式
const userStore = useUserStore()
直接修改原理
在optionsStore模式下:实际上是通过ObjectRefImpl数据内部代理的形式,实现对原对象的修改。
mainStore.count = 1
在前面我们就已经知道了optionsStore模式下的Store中有一行重要代码:
// pinia.state.value[id] 就是我们在mianStore刚开始的 state对象;{ count: 0 }
const localState = toRefs(pinia.state.value[id])
这里用toRefs方法:将原始的state对象作为参数,返回一个新对象,其属性都是转换后的ObjectRefImpl数据,了解toRefs方法源码的都知道,对ObjectRefImpl数据的设置,实际上都是会代理到原对象,即state对象。
class ObjectRefImpl<T extends object, K extends keyof T> {
...
set value(newVal) {
// 原理:修改原对象
this._object[this._key] = newVal
}
}
在setupStore模式下:因为注册时引用的同一个ref数据对象,所以通过userStore.age的修改,pinia.state.value.user.age也会变化,因为它们指向了同一个对象。
userStore.age = 1
案例验证:
const userStore = useUserStore()
const p = mainStore._p
// 验证: 指向同一个对象
console.log(p.state.value.user.age === userStore.age) // true
$patch方法修改原理
$patch批量修改方法原理就比较简单了,两种模式都是一样的原理:
# 修改state
// 1,对象参数
store.$patch({
count: store.count + 1,
age: 120,
name: 'DIO',
})
// 2,函数参数
store.$patch((state) => {
state.items.push({ name: 'shoes', quantity: 1 })
state.hasChanged = true
})
# patch方法
function $patch( partialStateOrMutator ): void {
// 声明Mutation对象
let subscriptionMutation: SubscriptionCallbackMutation<S>
// 暂停订阅:避免修改state的过程中频繁触发回调
isListening = isSyncListening = false
# 参数为函数的情况:
if (typeof partialStateOrMutator === 'function') {
// 直接调用函数,参数为id对应的state数据
partialStateOrMutator(pinia.state.value[$id])
subscriptionMutation = {
type: MutationType.patchFunction,
storeId: $id,
events: debuggerEvents as DebuggerEvent[],
}
} else {
# 参数为对象的情况:
// 调用mergeReactiveObjects方法,递归合并传入state,覆盖更新value[$id]的key值
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>
)
}
- 参数为函数的情况下:直接调用函数,参数为目标对象,直接修改属性即可。
- 参数为对象的情况下:递归处理,使用新对象的值,覆盖更新
value[$id][key]的值。
storeToRefs
storeToRefs的实现很简单, 基本上就是将store中的值取出来并且转为ref, 然后返回.
export function storeToRefs<SS extends StoreGeneric>(
store: SS
): StoreToRefs<SS> {
// See https://github.com/vuejs/pinia/issues/852
// It's easier to just use toRefs() even if it includes more stuff
if (isVue2) {
// @ts-expect-error: toRefs include methods and others
return toRefs(store)
} else {
store = toRaw(store)
const refs = {} as StoreToRefs<SS>
for (const key in store) {
const value = store[key]
if (isRef(value) || isReactive(value)) {
// @ts-expect-error: the key is state or getter
refs[key] =
// ---
toRef(store, key)
}
}
return refs
}
}