Pinia源码浅析
# Pinia 源码浅析
# 概述
Pinia
中只用到了 vue-demi 一种库,vue-demi 的介绍可以参考 vue-demi。
Pinia
可以在 vue2 和 vue3 中用于数据或状态的管理,同时 pinia 还提供了极其丰富的浏览器调试插件工具,更多详细内容可以参考 Pinia 官网 (opens new window)。
# vue2 中使用 Pinia
pinia
提供了PiniaVuePlugin
,在 vue2 中需要手动注册
import Vue from "vue";
import { PiniaVuePlugin, createPinia } from "pinia";
Vue.use(PiniaVuePlugin);
const pinia = createPinia();
new Vue({
el: "#app",
// ...
pinia,
});
2
3
4
5
6
7
8
9
10
11
# PiniaVuePlugin
原理
PiniaVuePlugin
是一个函数,注册时会被作为 install 方法,接收传入的参数 Vue,然后再通过Vue.mixin
全局混入breforeCreate
和destroyed
这两个生命周期钩子函数,影响到每一个 Vue 实例。
# breforeCreate
在这个钩子中 将选项中的 pinia 实例挂载到 vue 上。
beforeCreate() {
...
this._provided[piniaSymbol]=pinia
this.$pinia = pinia;
pinia._a=this //将vue挂载到pinia上,方便于pinia注册插件
...
}
2
3
4
5
6
7
# destroyed
在销毁组件时,将组件实例从 pinia 实例中移除。
# vue3 中使用 Pinia
vue3 中使用 Pinia 需要先调用createPinia
创建 Pinia 实例,返回 pinia 对象,再手动注册 Pinia 实例。
# createPinia
创建 Pinia 实例
createPinia
函数是Pinia
的核心函数,它返回一个Pinia
实例,该实例包含install
方法,用于安装Pinia
插件,以及use
方法,用于注册插件,比如数据持久化插件piniaPluginPersistedstate
createPinia
首先通过vueDemi.effectScope
创建一个独立的作用域scope
,再通过scope.run
方法返回一个vueDemi.ref({})
ref 对象作为state
由上可知 vue2 中使用Pinia
也会调用createPinia
方法创建实例,因此在install
属性方法中 Pinia 判断了当前环境是否是 vue2,如果不是,则执行app.config.globalProperties.$pinia = pinia;
将 pinia 实例挂载到 vue 实例上
export function createPinia() {
const pinia =vueDemi.markRaw({
install:(app)=>{/*...*/},
use:(plugin)=>{/*...*/},
_p,// 需要通过pinia.use的插件集合
_a:null // 指向vue
_e:scope,//独立作用域
_s:new Map(),
state, // 全局ref对象
}
return pinia
}
2
3
4
5
6
7
8
9
10
11
12
# defineStore
定义 Store
defineStore
定义 store,接受一个唯一的 id
、setup
和 配置项setupOptions
,其中定义并返回了useStore
函数,
如果 Pinia 中没有定义了同样的id
,就判断参数setup
是否是一个函数,如果是函数,则调用createSetupStore
,否则调用createOptionsStore
创建 store。
如果 Pinia 中已经定义了同名id
的 store,否则通过id
获取 store 并返回。
# createSetupStore
createSetupStore
函数是 Pinia 中的核心部分, 接收 6 个参数,分别是id
,setup
,options
,pinia
,hot
,isOptionsStore
。createSetupStore
函数返回一个 store,这个 store 是一个对象,包含$id
,$state
,$patch
,$reset
,$subscribe
,$subscribeWith
,$dispose
等方法。
# createOptionsStore
# storeToRefs
storeToRefs
接收一个参数store
,作用是从store
中解构出响应式数据。
pinia 内部实现storeToRefs
的逻辑是先判断当前环境是否是 vue2,
如果是 vu2,则通过vueDemi.toRef
工具函数将 store 转为响应式数据,然后返回;
如果当前是 vue3,则分为两步:
- 定义一个空对象 refs,通过
vueDemi.toRaw
将 store 转为普通对象并返回 - 循环遍历第 1 步中的对象,通过
vueDemi.isRef
和vueDemi.isReactive
判断其值是否是响应式数据,如果是,则通过vueDemi.toRef
将响应式数据转为普通数据,然后返回; - 返回 refs
# mapStores
mapStores
允许在不使用组合式 API (setup()
) 的情况下使用存储(stores),通过生成一个对象来在组件的computed
字段中进行扩展。它接受多个 store 实例,并返回一个对象,其中每个属性都对应一个 store 实例。我们可以通过 store 的 id+Store 来访问
用法示例
export default {
computed: {
...mapStores(useUserStore, useCartStore),
},
created() {
this.userStore.fetchUser(); //eg store with id "user"
this.cartStore.fetchCart(); //eg store with id "cart"
},
};
2
3
4
5
6
7
8
9
mapStores
的实现很简单,遍历参数,然后返回一个对象,key 是每一个 store 的 id 加上后缀Store
,value 是 store。后缀Store
是 Pinia 默认的,我们也可以通过 Pinia 暴露的setMapStoreSuffix
方法设置这个后缀。
function mapStores(...stores) {
return stores.reduce((reduced, useStore) => {
// @ts-expect-error: $id is added by defineStore
reduced[useStore.$id + mapStoreSuffix] = function () {
return useStore(this.$pinia);
};
return reduced;
}, {});
}
2
3
4
5
6
7
8
9
# mapState
mapState
,允许在不适用组合式 API 的情况下使用 state,接收两个参数,第一个参数是 store,第二个参数是 store 的 key。key 可以是字符串数组也可以是对象,如果是对象,对象的 key 是函数的情况下,则可能在 ts 中使用时报错,因为由于某种原因,TS 无法将 storeKey 的类型推断为函数。
mapState
等价于mapGetters
,不过是mapGetters
被 deprecated 弃用了。
用法示例如下
/*
state() => {
return {
count: 0,
message:"hello"
}
}
*/
export default defineComponent({
computed: {
...mapState(useCounterStore(), ["count", "message"]),
},
created() {
console.log(this.count, this.mesaage);
},
});
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
源码如下
function mapState(useStore, keysOrMapper) {
return Array.isArray(keysOrMapper)
? keysOrMapper.reduce((reduced, key) => {
reduced[key] = function () {
return useStore(this.$pinia)[key];
};
return reduced;
}, {})
: Object.keys(keysOrMapper).reduce((reduced, key) => {
// @ts-expect-error
reduced[key] = function () {
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.call(this, store)
: store[storeKey];
};
return reduced;
}, {});
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# mapWriteableState
mapWriteableState
允许在不使用组合式 API 的情况下,修改设置 state。因为 state 返回的是一个对象,可以利用重写对象的set
设置其值
其内部实现如下
function mapWritableState(useStore, keysOrMapper) {
return Array.isArray(keysOrMapper)
? keysOrMapper.reduce((reduced, key) => {
// @ts-ignore
reduced[key] = {
get() {
return useStore(this.$pinia)[key];
},
set(value) {
// it's easier to type it here as any
return (useStore(this.$pinia)[key] = value);
},
};
return reduced;
}, {})
: Object.keys(keysOrMapper).reduce((reduced, key) => {
// @ts-ignore
reduced[key] = {
get() {
return useStore(this.$pinia)[keysOrMapper[key]];
},
set(value) {
// it's easier to type it here as any
return (useStore(this.$pinia)[keysOrMapper[key]] = value);
},
};
return reduced;
}, {});
}
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
# mapActions
同上,通过mapActions
可以访问使用到 store 实例的 action 方法。mapActions
内部实现和mapState
逻辑大同小异。
使用示例如下
export default {
methods: {
...mapActions(useCounterStore, ["increment"]),
},
};
2
3
4
5
# skipHydrate
和shouldHydrate
skipHydrate
函数用于在热更新时跳过 hydrate
阶段。hydrate
,即水合,在 vue3 中,hydrate
是一个用于将 VNode 树转化为真实 DOM 的过程,它尝试复用服务器端渲染(SSR)生成现有 DOM 结构,这个过程是在客户端启动时进行的,目的是加快页面的渲染速度,减少首屏加载时间。
内部实现原理是首先判断当前环境,如果是 vue3,则给对象加一个skipHydrateSymbol
属性,反之给skipHydrateMap
追加一个键值对[obj]:1
,这样通过shouldHydrate
判断是否需要刷新该对象。
而shouldHydrate
在通过createSetupStore
创建 store 时会调用,判断是否需要进行水合
# acceptHMRUpdate
热模块替换函数
acceptHMRUpdate
函数接收两个参数,第一个参数是store
,第二个参数是hot
,即import.meta.hot
,acceptHMRUpdate
函数返回一个函数,这个函数接收一个参数,这个参数是store
,这个函数会判断store
是否是import.meta.hot
的module.$id
,如果是,则返回store
,否则返回null
。
内部实现原理:
订阅 HMR 更新:Pinia 在创建 store 实例时,会订阅 webpack 的 HMR API,以便在更新时接收到通知。
保存先前的状态:在接收到 HMR 更新之前,Pinia 会首先保存当前的状态。
应用更新:当收到 HMR 更新通知时,Pinia 会调用 acceptHMRUpdate 方法。
恢复状态:在应用更新之前,Pinia 会将状态回滚到先前保存的状态,以确保状态不会因为更新而丢失。
应用更新:Pinia 会应用收到的更新,例如添加、删除或修改状态。
重新应用状态:更新应用后,Pinia 会再次应用保存的状态,以确保在更新过程中不丢失任何状态。
通过这种方式,Pinia 可以安全地在开发过程中接受并应用 HMR 更新,从而保持应用程序的状态与代码的一致性,而不需要刷新整个页面。
使用示例如下:
const useUser = defineStore(...)
if (import.meta.hot) {
import.meta.hot.accept(acceptHMRUpdate(useUser, import.meta.hot))
}
2
3
4