vue pinia 新状态管理库
文章目录
官方文档: Introduction | Pinia
目录结构:
|
|
源码大致结构流程图:
从图中就可以看出来 pinia 核心的代码其实就在
createSetupStore()
这个函数里,它的实 现包含了 store 中的state
,getters
,actions
处理,以及用到的核心 api$patch
,$dispose
,$subscribe
,$reset
的实现和封装。
使用步骤:
安装:
$ yarn add pinia
或$ npm install pinia
引入 pinia
1 2 3
import { createPinia } from 'pinia' app.use(createPinia())
定义 store(如:
src/stores/user.ts
)1 2 3 4 5 6 7
import { defineStore } from 'pinia' export const useCounterStore = defineStore('counter', { state() { return { counter: 1, /* ... */ } } })
使用 store
1 2 3 4 5 6 7 8 9
// XxxUserComponent.ts import { useCounterStore } from '@/stores/counter' export default { setup() { const store = useCounterStore() return { store } } }
或非 setup() 函数中使用:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
import { mapState } from 'pinia' import { useCounterStore } from '@/stores/counter' export default { computed: { // 使用后组件中就可以通过 `this.counter` 直接读取值 ...mapState(useCounterStore, ['counter']), // 更高级灵活的使用, ...mapState(useCounterStore, { // 重命名, this.myOwnName 读取 myOwnName: 'counter', // 可以直接调用this.double() 来修改 store double: store => store.counter * 2, // 可以访问 store this 但是类型可以不能正确的被分析到 magicValue(store) { return store.someGetter + this.counter + this.double } }) } }
提供可以直接修改
this.counter++
的能力:1 2 3 4 5 6 7 8 9 10 11 12 13
import { mapWritableState } from 'pinia' import { useCounterStore } from '../stores/counterStore' export default { computed: { // 具备可以直接 this.counter++ 读取且修改 store 能力 ...mapWritableState(useCounterStore, ['counter']) // 如上, this.myOwnName++ ...mapWritableState(useCounterStore, { myOwnName: 'counter', }), }, }
修改 state 方式
store.$patch(payload)
:1
store.$patch({ counter: store.counter + 1, name: 'xxx' })
store.$patch(stateFn)
:1 2 3 4
store.$patch(state => { state.items.push({ name: 'shoes', quantity: 1 }) state.hasChanged = true })
直接替换
store.$state
store.$state = { counter: 666, name: 'Paimon' }
直接替换整个 app 的 state:
1
pinia.state.value = {}
订阅 state 变更(
store.$subscribe
或watch(pinia.state, state=>{...},{deep:true})
1 2 3 4 5 6 7 8 9 10 11
cartStore.$subscribe((mutation, state) => { // import { MutationType } from 'pinia' mutation.type // 'direct' | 'patch object' | 'patch function' // same as cartStore.$id mutation.storeId // 'cart' // only available with mutation.type === 'patch object' mutation.payload // patch object passed to cartStore.$patch() // persist the whole state to the local storage whenever it changes localStorage.setItem('cart', JSON.stringify(state)) })
component setup() 中使用,卸载时移除,如果不想被移除需要指定
{detached:true}
options1 2 3 4 5 6 7 8 9 10
export default { setup() { const someStore = useSomeStore() // this subscription will be kept after the component is unmounted someStore.$subscribe(callback, { detached: true }) // ... }, }
监听整个 app state 变化:
1 2 3 4 5 6 7 8
watch( pinia.state, (state) => { // persist the whole state to the local storage whenever it changes localStorage.setItem('piniaState', JSON.stringify(state)) }, { deep: true } )
createPinia()
创建 pinia 实例,在调用 Vue.use(createPinia)
之后,就可以在组件内通过
this.$pinia
来调用这个实例,因为它会将 pinia 实例挂到
app.config.globalProperties.$pinia
上。
|
|
defineStore()
声明:
|
|
上面三种声明对应下面三种使用方式:
|
|
API 实现
defineStore(idOrOptions: any, setup?: any, setupOptions?: any): StoreDefinition
|
|
defineStore()
实现就两部分,一个是解析参数来支持多种调用方式,一个是实现
useStore()
这个也是其核心代码。
|
|
取出 pinia 在当前应用中的实例
pinia = pinia || (currentInstance && inject(piniaSymbol))
inject(piniaSymbol)
是取出在app.use(createPinia())
中注入的一个应用全局变量:代码: src/createPinia.ts:createPinia()
state 定义:
1 2 3
const state = scope.run<Ref<Record<string, StateTree>>>(() => ref<Record<string, StateTree>>({}) )!
注入(
pinia.install(app)
):1 2
app.provide(piniaSymbol, pinia) app.config.globalProperties.$pinia = pinia
检查是不是每次调用
useStore()
1 2 3 4 5 6 7 8
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) } }
两和创建方式,一个是
setup()
函数式,一个{ state() {} }
对象方式。defineStore('storeId', function storeSetup() {...}, { ...options }
defineStore('storeId', { state() {}, ...otherOptions })
createOptionsStore()
最终也是会调用createSetupStore()
所以创建 store 的核心 代码在后者。
createSetupStore($id, setup, options, pinia, hot?)
声明:
|
|
简化版:
|
|
初始化 state
非首次使用时,
pinia.state.value
中会有当前 store 记录,直接取出,如果没有默 认初始值为undefined
:const initialState = pinia.state.value[$id] as UnwrapRef<S> | undefined
1 2 3 4 5 6 7 8 9 10
// avoid setting the state for option stores are it is set // by the setup if (!buildState && !initialState && (!__DEV__ || !hot)) { /* istanbul ignore if */ if (isVue2) { set(pinia.state.value, $id, {}) } else { pinia.state.value[$id] = {} } }
这里区分 vue2/vue3, 因为 vue2 无法直接监听对象新增成员,所以要用
set()
函数, 而 vue3 是使用proxy+reflect
实现的 reactivity 因此直接赋值即可。$patch(partialStateOrMutator)
用来更新 state 状态1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36
function $patch( partialStateOrMutator: | _DeepPartial<UnwrapRef<S>> | ((state: UnwrapRef<S>) => void) ): void { let subscriptionMutation: SubscriptionCallbackMutation<S> isListening = isSyncListening = false // 分析参数,支持 $patch({...}) 或 $patch(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[], } } nextTick().then(() => { 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> ) }
$patch()
支持两种调用方式:$patch({...})
或$patch(state=>{...})
封装
mutation
手动触发 reactivity trigger1 2 3 4 5 6 7 8
export function triggerSubscriptions<T extends _Method>( subscriptions: T[], ...args: Parameters<T> ) { subscriptions.slice().forEach((callback) => { callback(...args) }) }
手动调用
subscriptions
这是个数组,会在调用$subscribe()
时候收集订阅。$reset()
函数$dispose()
释放当前的 store,会从pinia._s
中删除它并清理subscriptions
1 2 3 4 5 6
function $dispose() { scope.stop() subscriptions = [] actionSubscriptions = [] pinia._s.delete($id) }
wrapAction(name: string, action: _Method)
封装 action。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49
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) } // @ts-expect-error triggerSubscriptions(actionSubscriptions, { args, name, store, after, onError, }) let ret: any try { 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) => { triggerSubscriptions(afterCallbackList, value) return value }) .catch((error) => { triggerSubscriptions(onErrorCallbackList, error) return Promise.reject(error) }) } // allow the afterCallback to override the return value triggerSubscriptions(afterCallbackList, ret) return ret } }
partialStore & store 对象
对象包含:
{ _p: pinia, $id, $onAction, $patch, $reset, $subscribe, $dispose }
$onAction: addSubscription.bind(null, actionSubscriptions)
添加订阅用。1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
$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 }
store:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
const store: Store<Id, S, G, A> = reactive( assign( __DEV__ && IS_CLIENT ? // devtools custom properties { _customProperties: markRaw(new Set<string>()), _hmrPayload, } : {}, partialStore // must be added later // setupStore ) ) as unknown as Store<Id, S, G, A>
执行 setup 得到其返回值做为 state
1 2 3 4 5
// TODO: idea create skipSerialize that marks properties as non serializable and they are skipped const setupStore = pinia._e.run(() => { scope = effectScope() return scope.run(() => setup()) })!
对 setup state 进行加工处理,如 wrap action
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43
// overwrite existing actions to support $onAction for (const key in setupStore) { const prop = setupStore[key] // 检查值是不是 Ref 或 Reactive 类型 if ((isRef(prop) && !isComputed(prop)) || isReactive(prop)) { // options.state if (!buildState) { // in setup stores we must hydrate the state and sync pinia state tree with the refs the user just created if (initial 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 /* istanbul ignore if */ if (isVue2) { set(pinia.state.value[$id], key, prop) } else { pinia.state.value[$id][key] = prop } } } 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 } // list actions so they can be used in plugins // @ts-expect-error optionsForPlugin.actions[key] = prop } // ... __DEV__
经过步骤 7 之后,将
setupStore
合并到store
和toRaw(store)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
if (isVue2) { Object.keys(setupStore).forEach((key) => { set( store, key, // @ts-expect-error: valid key indexing 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) }
这一行
assign(toRaw(store), setupStore)
是考虑到可能需要解构store
需要用到storeToRefs()
的情况。最后定义
store.$state
的读写操作1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
// 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. 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) }) }, })
经过上面的分析下来, defineStore
-> createSetupStore
中关键几个函数;
$patch(partialStateOrMutator)
批量更新 state 触发 triggerSubscriptions(
subscriptions )
执行, state 递归合并原则(mergeRectiveObjects
)。
$subscribe(callback, options={})
订阅 state(pinia.state.value[$id]
) 的变化,其实
现也是通过 watch()
监听 pinia.state.value[$id]
实现。
createOptionsStore(id, options, pinia, hot?)
这个函数是对 defineStore(id, options)
或 defineStore(options)
的 options 是对象时的处理逻辑,它最终也是调用
了 createSetupStore() 是将 options 封装成了 setup 函数。
|
|
所以对于 options 的使用其实也是将 { state, actions, getters }
简单的
assign(...)
合并在一起了,同时对 getters
进行了一层封装,将它们转成了计算属性。
options api(mapHelpers)
mapGetters=mapState(useStore, keysOrMapper)
方式一:
|
|
方式二:
|
|
实现:
|
|
如果是: ['counter', 'name']
数组形式:
|
|
如果是对象(成员值是函数执行返回结果,如果值不是函数用这个值做为 key 去 store 中 取值相当于别名):
|
|
mapActions(useStore,keysOrMapper)
实现原理和 mapState
一样,无非是 useStore(this.$pinia)[key](...args)
取到对应的
函数后执行它。
|
|
mapWritableState(useStore,keysOrMapper)
通过重写成员值的访问器函数实现。
|
|
storeToRefs(store)
|
|