Pinia 部分源码浅析
概述
Pinia
中只用到了 vue-demi 一种库,vue-demi 的介绍可以参考 vue-demi。
Pinia
可以在 vue2 和 vue3 中用于数据或状态的管理,同时 pinia 还提供了极其丰富的浏览器调试插件工具,更多详细内容可以参考 Pinia 官网。
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,
});
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注册插件
...
}
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
}
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"
},
};
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;
}, {});
}
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);
},
});
源码如下
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;
}, {});
}
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;
}, {});
}
mapActions
同上,通过mapActions
可以访问使用到 store 实例的 action 方法。mapActions
内部实现和mapState
逻辑大同小异。
使用示例如下
export default {
methods: {
...mapActions(useCounterStore, ["increment"]),
},
};
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))
}