介绍
Pinia意为菠萝,表示与菠萝一样,由很多小块组成。在pinia中,每个store都是单独存在,一同进行状态管理。
很多人也将pinia称为vuex5,因为pinia将作为vue3推荐的状态管理库,而vuex将不再迭代。主要原因按pinia原文是:
Eventually, we realized that Pinia already implements most of what we wanted in Vuex 5, and decided to make it the new recommendation instead.
用例
// 创建pinia实例并挂载(vue3)
import { createPinia } from 'pinia'
app.use(createPinia())
// 定义
import { defineStore } from 'pinia'
export const useCounterStore = defineStore('counter', {
state: () => {
return { count: 0 }
},
// could also be defined as
// state: () => ({ count: 0 })
getters: {
double: (state) => state.count * 2,
},
actions: {
increment() {
this.count++
},
},
})
// 使用
import { useCounterStore } from '@/stores/counter'
export default {
setup() {
const counter = useCounterStore()
counter.count++
// with autocompletion ✨
counter.$patch({ count: counter.count + 1 })
// or using an action instead
counter.increment()
},
}
// 也可以使用类似于vuex的mapState等方式引入
import { useCounterStore } from '@/stores/counter'
export default {
computed: {
// other computed properties
// ...
// gives access to this.counterStore
...mapStores(useCounterStore),
// gives read access to this.count and this.double
...mapState(useCounterStore, ['count', 'double']),
},
methods: {
// gives access to this.increment()
...mapActions(useCounterStore, ['increment']),
},
}
问题
- pinia是如何做到类型安全,摒弃魔法字符串?
- 为什么pinia不需要mutation?
- 为什么$patch可以批量更新?
前置知识
Composition: 是指通过函数编写vue组件。类似react的hook组件。
setup(): 在optionAPI中使用Composition,需要通过setup函数绑定到this上。
源码阅读
# 目录
.
├── createPinia.ts
├── devtools
│ ├── actions.ts
│ ├── file-saver.ts
│ ├── formatting.ts
│ ├── index.ts
│ ├── plugin.ts
│ └── utils.ts
├── env.ts
├── global.d.ts
├── globalExtensions.ts
├── hmr.ts
├── index.ts
├── mapHelpers.ts
├── rootStore.ts
├── store.ts
├── storeToRefs.ts
├── subscriptions.ts
├── types.ts
└── vue2-plugin.ts
createPinia.ts
createPinia.ts中只有一个函数,就是createPinia()
,主要工作是挂载全局pinia实例
export function createPinia(): Pinia {
// effectScope:创建一个能够使用响应式函数的作用域
// https://vuejs.org/api/reactivity-advanced.html#effectscope
const scope = effectScope(true)
// 创建一个具有响应式的对象,用于存储store
const state = scope.run<Ref<Record<string, StateTree>>>(() =>
ref<Record<string, StateTree>>({})
)!
// 插件队列
let _p: Pinia['_p'] = []
// plugins added before calling app.use(pinia)
// 在pinia挂载前添加的插件
let toBeInstalled: PiniaPlugin[] = []
// markRaw:创建不可响应式的对象
// https://vuejs.org/api/reactivity-advanced.html#markraw
const pinia: Pinia = markRaw({
// 将pinia实例注册到Vue实例中
// Vue.use将调用该方法进行注册
install(app: App) {
// 设置当前pinia实例
setActivePinia(pinia)
if (!isVue2) {
pinia._a = app
// 创建store时获取
app.provide(piniaSymbol, pinia)
// 挂载全局pinia实例
app.config.globalProperties.$pinia = pinia
// 将pinia插件都加入_p对列中
toBeInstalled.forEach((plugin) => _p.push(plugin))
toBeInstalled = []
}
},
use(plugin) {
if (!this._a && !isVue2) {
// 未pinia挂载前添加的插件
toBeInstalled.push(plugin)
} else {
_p.push(plugin)
}
return this
},
_p,
_a: null,
_e: scope,
_s: new Map<string, StoreGeneric>(),
state,
})
return pinia
}
store.ts
里面暴露给用户defineStore()
,skipHydrate()
两个函数。
defineStore文档
其中defineStore将创建useStore
函数。
useStore的作用是:
- 获取createPinia时,provide提供的pinia实例
- 根据id在pinia实例中注册store
- 返回store
function useStore(pinia?: Pinia | null, hot?: StoreGeneric): StoreGeneric {
const currentInstance = getCurrentInstance()
// 获取createPinia时,provide提供的pinia实例
pinia =
(currentInstance && inject(piniaSymbol))
if (pinia) setActivePinia(pinia)
// 省略 错误处理activePinia缺失
pinia = activePinia!
// 当前id是否已注册
if (!pinia._s.has(id)) {
// 根据id注册store
// 判断defineStore传入的第二个参数是否函数
if (isSetupStore) {
createSetupStore(id, setup, options, pinia)
} else {
createOptionsStore(id, options as any, pinia)
}
}
// 获取当前store
const store: StoreGeneric = pinia._s.get(id)!
// 省略 如果是热更新重新创建store替代旧store
// 省略 在实例中存储store,以便devtool访问
return store as any
}
注册store时,如果是传入非function,将会执行createOptionsStore()方法。
该方法主要是将传入的参数整理成createSetupStore所需的形式并调用。
function createOptionsStore() {
const { state, actions, getters } = options
const initialState: StateTree | undefined = pinia.state.value[id]
let store: Store<Id, S, G, A>
// setup函数抽离到下方
store = createSetupStore(id, setup, options, pinia, hot, true)
store.$reset = function $reset() {
// 使用初始的state函数重置状态
const newState = state ? state() : {}
// 使用$patch一次通知所有改变
this.$patch(($state) => {
// 将原state与现有state合并,将state存在的属性值重置
assign($state, newState)
})
}
return store as any
}
createSetupStore函数是pinia最主要的函数。所以我们需要更仔细地查阅。
// 省略 通过$subscribeOptions收集debuggerEvents
// internal state
let isListening: boolean // 表示当前正在更新中,防止触发太多次更新,为true时更新结束
let isSyncListening: boolean // 同上,但用于同步更新
// 下面几个都是订阅的事件
let subscriptions: SubscriptionCallback<S>[] = markRaw([])
let actionSubscriptions: StoreOnActionListener<Id, S, G, A>[] = markRaw([])
let debuggerEvents: DebuggerEvent[] | DebuggerEvent
// 初始值
const initialState = pinia.state.value[$id] as UnwrapRef<S> | undefined
// 省略 非createOptionsStore初始化状态
接下来就是对state与action的处理了
// 调用setup函数
const setupStore = pinia._e.run(() => {
scope = effectScope()
return scope.run(() => setup())
})!
// setup函数会设置store初始值,将state、getter、action都合并到一起,
// 接下来在createSetupStore中会根据类型进行区分并执行不同的操作。将getter包裹在computed中。
function setup() {
// 根据pinia.state.value[id]与options.state设置初始值
if (!initialState && (!__DEV__ || !hot)) {
if (isVue2) {
set(pinia.state.value, id, state ? state() : {})
} else {
pinia.state.value[id] = state ? state() : {}
}
}
// 防止因为响应式在pinia.state.value中创建多余元素
// https://vuejs.org/api/reactivity-utilities.html#torefs
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)!
// allow cross using stores
/* istanbul ignore next */
if (isVue2 && !store._r) return
// 指定this为当前store
return getters![name].call(store, store)
})
)
return computedGetters
}, {} as Record<string, ComputedRef>)
)
}
for (const key in setupStore) {
const prop = setupStore[key]
// 状态status
if ((isRef(prop) && !isComputed(prop)) || isReactive(prop)) {
// 省略 对状态的处理
// action
} else if (typeof prop === 'function') {
const actionValue = __DEV__ && hot ? prop : wrapAction(key, prop)
if (isVue2) {
set(setupStore, key, actionValue)
} else {
setupStore[key] = actionValue
}
optionsForPlugin.actions[key] = prop
} else if (__DEV__) {
// 省略 为devtool处理getter
}
}
wrapAction在开始 结束 抛错时都会触发triggerSubscriptions,通知相应订阅action的方法。
在执行action时,会判断是否promise,如果是pormise就会在promise结果后再触发更新,这也就是为什么异步的action也可以触发更新。
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
}
}
Store定义了一些方法,包括onAction订阅store的action事件触发、patch与$subscribe。
$subscribe用于订阅状态的修改
$subscribe(callback, options = {}) {
// 这里我调换了顺序,原代码stopWatcher在下方
const stopWatcher = scope.run(() =>
watch(
() => pinia.state.value[$id] as UnwrapRef<S>,
(state) => {
// 当数据更新,只有isListening为ture时触发更新
if (options.flush === 'sync' ? isSyncListening : isListening) {
callback(
{
storeId: $id,
type: MutationType.direct,
events: debuggerEvents as DebuggerEvent,
},
state
)
}
},
assign({}, $subscribeOptions, options)
)
)!
// addSubscription是封装的一个发布订阅模式
const removeSubscription = addSubscription(
subscriptions,
callback,
options.detached,
() => stopWatcher()
)
return removeSubscription
}
$patch用于批量更新数据,防止多次触发更新。
里面用到的mergeReactiveObjects是通过递归实现深度遍历对象合并对象。
let activeListener: Symbol | undefined
function $patch(stateMutation: (state: UnwrapRef<S>) => void): void
function $patch(partialState: _DeepPartial<UnwrapRef<S>>): void
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') {
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
// 因为通过设置isListening暂停了更新,所以在最后需要手动触发一次
triggerSubscriptions(
subscriptions,
subscriptionMutation,
pinia.state.value[$id] as UnwrapRef<S>
)
}
// 对于$state进行监听,使用$patch批量更新
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)
})
},
})
回答
- pinia是如何做到类型安全,摒弃魔法字符串?
因为pinia将state、getter、action都绑定到store的属性,通过composition可以获取到对应的store实例。 - 为什么pinia不需要mutation?
在vuex中使用mutation记录数据的更新,action进行异步操作,而pinia在action执行完成后会自动发布给订阅者,所以就不需要mutation。 - 为什么$patch可以批量更新?
在更新期间通过isListening标识暂停触发回调函数。