pinia入门指北
pinia是个什么东西
前言
Pinia 是 Vue.js 团队成员专门为 Vue 开发的一个全新的状态管理库,并且已经被纳入官方 github。
为什么有了vueX,还要开发pinia?
这是vueX 5 的提案。
而pinia刚好符合vueX5提案的所有要求,事实上,pinia也是基于vueX对未来状态管理器的一种探索。
简介
Pinia.js 是新一代的状态管理器,由 Vue.js团队中成员所开发的,因此也被认为是下一代的 Vuex,即 Vuex5.x,在 Vue3.0 的项目中使用也是备受推崇。
Pinia刚开始只是一个实验,旨在重新设计Vue的Store在2019年11月左右可以使用Composition API的样子。从那时起,最初的原则仍然是相同的,但Pinia同时适用于Vue 2和Vue 3,并且不要求你使用composition API。
核心特性
state
state是pinia的核心部分,所有需要管理的状态属性都写到这里,和vueX的state差不多,是唯一存储能够存储数据的地方,并且具有响应式。
基本使用方式如下:
import { defineStore } from 'pinia'
const useStore = defineStore('storeId', {
return {
counter: 0,
name: 'Eduardo',
isAdmin: true,
}
},
})
可以使用mapState() 引入多个数据,只能读取数据; 也可以使用mapWritableState() 引入多个数据,并且可以读取和修改数据。 修改数据:
// 1、可以直接以点操作符的方式修改
const store = useStore():
store.counter++
// 2、可以使用$patch()
store.$patch({
counter: store.counter + 1,
name: 'Abalam',
})
// 3、也可以调用action里面的方法去修改
监听state的改变
store.$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('store', JSON.stringify(state))
})
getters
和vuex的getters一样 ,也具有缓存性,相当于pinia的计算属性,事实上他也是用vue的计算属性来实现的。
基本使用方式如下:
export const useStore = defineStore('main', {
state: () => ({
counter: 0,
}),
getters: {
doubleCount: (state) => state.counter * 2,
myCount(): number{
return this.count + 1
}
},
})
actions
pinia废弃了mutation,可以使用action代替mutation,也就是说可以在action里面写同步代码和异步代码。
基本使用方式如下:
export const useStore = defineStore('main', {
state: () => ({
counter: 0,
}),
actions: {
increment() {
this.counter++
},
asyncIncrement() {
setTimeout(() => {
this.counter += 5;
}, 1000);
},
},
})
pinia有什么优点和缺点
优点
-
支持devtools
-
支持hmr热更新
-
支持pinia插件扩展
-
更好的 typescript 的支持;
不需要再创建自定义的复杂包装器来支持 TypeScript 所有内容都类型化,并且 API 的设计方式也尽可能的使用 TS 类型推断 -
支持ssr服务端渲染;
-
足够轻量,压缩后的体积只有1.6kb;
-
去除 mutations、module,只有 state、getters、actions;actions 支持同步和异步;
-
没有模块嵌套,只有 store 的概念,store 之间可以自由使用,更好的代码分割;
Pinia 通过设计提供扁平结构,就是说每个 store 都是互相独立的,谁也不属于谁,也就是扁平化了,更好的代码分割且没有命名空间。当然你也可以通过在一个模块中导入另一个模块来隐式嵌套 store,甚至可以拥有 store 的循环依赖关系pinia和vuex相比,一些比较突出实用的功能
actions 可以写同步代码和异步代码
没有module模块 扁平化结构,每个容器之间又可以相互引用
代码更加简洁
可以针对某个容器做定制化持久化存储
缺点
暂时还没发现。
pinia的基本使用
pinia核心源码分享
createPinia
源码位置:packages/pinia/src/createPinia.ts
PiniaVuePlugin - vue2
源码位置:packages/pinia/src/vue2-plugin.ts
vueX 3.0 - install() 部分代码
export function install (_Vue) {
if (Vue && _Vue === Vue) {
if (__DEV__) {
console.error(
'[vuex] already installed. Vue.use(Vuex) should be called only once.'
)
}
return
}
Vue = _Vue
applyMixin(Vue)
}
export default function (Vue) {
const version = Number(Vue.version.split('.')[0])
if (version >= 2) {
Vue.mixin({ beforeCreate: vuexInit })
} else {
// override init and inject vuex init procedure
// for 1.x backwards compatibility.
const _init = Vue.prototype._init
Vue.prototype._init = function (options = {}) {
options.init = options.init
? [vuexInit].concat(options.init)
: vuexInit
_init.call(this, options)
}
}
/**
* Vuex init hook, injected into each instances init hooks list.
*/
function vuexInit () {
const options = this.$options
// store injection
if (options.store) {
this.$store = typeof options.store === 'function'
? options.store()
: options.store
} else if (options.parent && options.parent.$store) {
this.$store = options.parent.$store
}
}
}
defineStore
源码地址: packages\pinia\src\store.ts
├─defineStore
│ ├─createSetupStore
│ ├─createOptionsStore
defineStore -> useStore -- createSetupStore() --> store
vueX 3.0 - 响应式 部分代码
function resetStoreVM (store, state, hot) {
const oldVm = store._vm
// bind store public getters
store.getters = {}
// reset local getters cache
store._makeLocalGettersCache = Object.create(null)
const wrappedGetters = store._wrappedGetters
const computed = {}
forEachValue(wrappedGetters, (fn, key) => {
// use computed to leverage its lazy-caching mechanism
// direct inline function use will lead to closure preserving oldVm.
// using partial to return function with only arguments preserved in closure environment.
computed[key] = partial(fn, store)
Object.defineProperty(store.getters, key, {
get: () => store._vm[key],
enumerable: true // for local getters
})
})
// use a Vue instance to store the state tree
// suppress warnings just in case the user has added
// some funky global mixins
const silent = Vue.config.silent
Vue.config.silent = true
store._vm = new Vue({
data: {
$$state: state
},
computed
})
Vue.config.silent = silent
// enable strict mode for new vm
if (store.strict) {
enableStrictMode(store)
}
if (oldVm) {
if (hot) {
// dispatch changes in all subscribed watchers
// to force getter re-evaluation for hot reloading.
store._withCommit(() => {
oldVm._data.$$state = null
})
}
Vue.nextTick(() => oldVm.$destroy())
}
}
store.$onAction
$onAction: addSubscription.bind(null, actionSubscriptions),
$onAction内部通过发布订阅模式实现。在pinia中有个专门的订阅模块subscriptions.ts,其中包含两个主要方法:addSubscription(添加订阅)、triggerSubscriptions(触发订阅)。
如何触发订阅
首先在store的初始化过程中,会将action使用wrapAction函数进行包装,wrapAction返回一个函数,在这个函数中会先触发actionSubscriptions,这个触发过程中会将afterCallback、onErrorCallback添加到对应列表。然后调用action,如果调用过程中出错,则触发onErrorCallbackList,否则触发afterCallbackList。如果action的结果是Promise的话,则在then中触发onErrorCallbackList,在catch中触发onErrorCallbackList。然后会将包装后的action覆盖原始action,这样每次调用action时就是调用的包装后的action。
store.$patch
function $patch(
partialStateOrMutator:
| _DeepPartial<UnwrapRef<S>>
| ((state: UnwrapRef<S>) => void)
): void {
// 合并的相关信息
let subscriptionMutation: SubscriptionCallbackMutation<S>
// 是否触发状态修改后的回调,isListening代表异步触发,isSyncListening代表同步触发
// 此处先关闭回调的触发,防止修改state的过程中频繁触发回调
isListening = isSyncListening = false
if (__DEV__) {
debuggerEvents = []
}
// 如果partialStateOrMutator是个function,执行方法,传入当前的store
if (typeof partialStateOrMutator === 'function') {
partialStateOrMutator(pinia.state.value[$id] as UnwrapRef<S>)
subscriptionMutation = {
type: MutationType.patchFunction,
storeId: $id,
events: debuggerEvents as DebuggerEvent[],
}
} else { // 如果不是function,则调用mergeReactiveObjects合并state
mergeReactiveObjects(pinia.state.value[$id], partialStateOrMutator)
subscriptionMutation = {
type: MutationType.patchObject,
payload: partialStateOrMutator,
storeId: $id,
events: debuggerEvents as DebuggerEvent[],
}
}
// 当合并完之后,将isListening、isSyncListening设置为true,意味着可以触发状态改变后的回调函数了
const myListenerId = (activeListener = Symbol())
nextTick().then(() => {
if (activeListener === myListenerId) {
isListening = true
}
})
isSyncListening = true
// 因为在修改pinia.state.value[$id]的过程中关闭(isSyncListening与isListening)了监听,所以需要手动触发订阅列表
triggerSubscriptions(
subscriptions,
subscriptionMutation,
pinia.state.value[$id] as UnwrapRef<S>
)
}
store.$reset
// 只在options配置下有效
store.$reset = function $reset() {
// 重新执行state,获取一个新的state
const newState = state ? state() : {}
// 通过$patch,使用assign将newState合并到$state中
this.$patch(($state) => {
assign($state, newState)
})
}
store.$subscribe
$subscribe(callback, options = {}) {
// 将callback添加到subscriptions中,以便使用$patch更新状态时,触发回调
// 当使用removeSubscription移除callback时,停止对pinia.state.value[$id]监听
const removeSubscription = addSubscription(
subscriptions,
callback,
options.detached,
() => stopWatcher()
)
const stopWatcher = scope.run(() =>
// 监听pinia.state.value[$id],以触发callback,当使用$patch更新state时,不会进入触发这里的callback
watch(
() => pinia.state.value[$id] as UnwrapRef<S>,
(state) => {
// options.flush === 'sync' 是否同步触发回调
if (options.flush === 'sync' ? isSyncListening : isListening) {
callback(
{
storeId: $id,
type: MutationType.direct, // 表示是通过什么方式更新的state
events: debuggerEvents as DebuggerEvent,
},
state
)
/** MutationType.direct:通过state.name='xxx'/store.$state.name='xxx'等方式修改
MutationType.patchObject:通过store.$patch({ name: 'xxx' })方式修改
MutationType.patchFunction:通过store.$patch((state) => state.name='xxx')方式修改
*/
}
},
assign({}, $subscribeOptions, options)
)
)!
return removeSubscription
},
store.$dispose
销毁pinia容器
function $dispose() {
// 停止监听
scope.stop()
// 清空subscriptions及actionSubscriptions
subscriptions = []
actionSubscriptions = []
// 从pinia._s中删除store
pinia._s.delete($id)
}
总结
defineStore返回一个useStore函数,通过执行useStore可以获取对应的store。调用useStore时我们并没有传入id,为什么能准确获取store呢?这是因为useStore是个闭包,在执行useStore执行过程中会自动获取id。
获取store的过程:
- 首先获取组件实例
- 使用
inject(piniaSymbol)获取pinia实例 - 判断
pinia._s中是否有对应id的键,如果有直接取对应的值作为store,如果没有则创建store
store创建流程分两种:setup方式与options方式
setup方式:
- 首先在
pinia.state.value中添加键为$id的空对象,以便后续赋值 - 使用
reactive声明一个响应式对象store - 将
store存至pinia._s中 - 执行
setup获取返回值setupStore - 遍历
setupStore的键值,如果值是ref(不是computed)或reactive,将键值添加到pinia.state.value[$id]中;如果值时function,首先将值使用wrapAction包装,然后用包装后的function替换setupStore中对应的值 - 将
setupStore合并到store中 - 拦截
store.$state,使get操作可以正确获取pinia.state.value[$id],set操作使用this.$patch更新 - 调用
pinia._p中的扩展函数,扩展store
options方式:
- 从
options中提取state、getter、actions - 构建
setup函数,在setup函数中会将getter处理成计算属性 - 使用
setup方式创建store - 重写
store.$reset
storeToRefs
当使用store的过程中,如果直接对store进行解构,会破坏数据的响应,所以pinia提供了storeToRefs用来进行解构。
export function storeToRefs<SS extends StoreGeneric>(
store: SS
): ToRefs<
StoreState<SS> & StoreGetters<SS> & PiniaCustomStateProperties<StoreState<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的原始对象
store = toRaw(store)
const refs = {} as ToRefs<
StoreState<SS> &
StoreGetters<SS> &
PiniaCustomStateProperties<StoreState<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
}
}
mapHelper
源码位置:packages\pinia\src\mapHelpers.ts
mapState
export function mapState<
Id extends string,
S extends StateTree,
G extends _GettersTree<S>,
A
>(
useStore: StoreDefinition<Id, S, G, A>,
keysOrMapper: any
): _MapStateReturn<S, G> | _MapStateObjectReturn<Id, S, G, A> {
return Array.isArray(keysOrMapper)
? keysOrMapper.reduce((reduced, key) => {
reduced[key] = function (this: ComponentPublicInstance) {
return useStore(this.$pinia)[key]
} as () => any
return reduced
}, {} as _MapStateReturn<S, G>)
: Object.keys(keysOrMapper).reduce((reduced, key: string) => {
// @ts-expect-error
reduced[key] = function (this: ComponentPublicInstance) {
const store = useStore(this.$pinia)
const storeKey = keysOrMapper[key]
// for some reason TS is unable to infer the type of storeKey to be a
// function
return typeof storeKey === 'function'
? (storeKey as (store: Store<Id, S, G, A>) => any).call(this, store)
: store[storeKey]
}
return reduced
}, {} as _MapStateObjectReturn<Id, S, G, A>)
}
mapWritableState
export function mapWritableState<
Id extends string,
S extends StateTree,
G extends _GettersTree<S>,
A,
KeyMapper extends Record<string, keyof S>
>(
useStore: StoreDefinition<Id, S, G, A>,
keysOrMapper: Array<keyof S> | KeyMapper
): _MapWritableStateReturn<S> | _MapWritableStateObjectReturn<S, KeyMapper> {
return Array.isArray(keysOrMapper)
? keysOrMapper.reduce((reduced, key) => {
// @ts-ignore
reduced[key] = {
get(this: ComponentPublicInstance) {
return useStore(this.$pinia)[key]
},
set(this: ComponentPublicInstance, value) {
// it's easier to type it here as any
return (useStore(this.$pinia)[key] = value as any)
},
}
return reduced
}, {} as _MapWritableStateReturn<S>)
: Object.keys(keysOrMapper).reduce((reduced, key: keyof KeyMapper) => {
// @ts-ignore
reduced[key] = {
get(this: ComponentPublicInstance) {
return useStore(this.$pinia)[keysOrMapper[key]]
},
set(this: ComponentPublicInstance, value) {
// it's easier to type it here as any
return (useStore(this.$pinia)[keysOrMapper[key]] = value as any)
},
}
return reduced
}, {} as _MapWritableStateObjectReturn<S, KeyMapper>)
}
mapActions
export function mapActions<
Id extends string,
S extends StateTree,
G extends _GettersTree<S>,
A,
KeyMapper extends Record<string, keyof A>
>(
useStore: StoreDefinition<Id, S, G, A>,
keysOrMapper: Array<keyof A> | KeyMapper
): _MapActionsReturn<A> | _MapActionsObjectReturn<A, KeyMapper> {
return Array.isArray(keysOrMapper)
? keysOrMapper.reduce((reduced, key) => {
// @ts-expect-error
reduced[key] = function (
this: ComponentPublicInstance,
...args: any[]
) {
return useStore(this.$pinia)[key](...args)
}
return reduced
}, {} as _MapActionsReturn<A>)
: Object.keys(keysOrMapper).reduce((reduced, key: keyof KeyMapper) => {
// @ts-expect-error
reduced[key] = function (
this: ComponentPublicInstance,
...args: any[]
) {
return useStore(this.$pinia)[keysOrMapper[key]](...args)
}
return reduced
}, {} as _MapActionsObjectReturn<A, KeyMapper>)
}
mapGetter
export const mapGetters = mapState
总结
mapStores、mapState、mapActions等辅助函数会在内部通过调用useStore(在useStore调用时会传入this.$pinia,this为组件实例,这也是为什么辅助函数不能用在setup中,因为setup中是无法获取组件实例)获取store,然后在store中获取对应属性。