Pinia的使用与原理
Pinia 与 Vuex 对比
-
vuex:
- ts兼容性不好
- 命名空间的缺陷(只能有一个store)
- mutation和action有区别
-
pinia:
- 更简洁的API
- ts兼容性更好
- 无命名空间的缺陷(可以创建多个store)
- 删除了mutation,统一在action中开发
使用方法
引入
// main.ts
import { createApp } from "vue";
import App from "./App.vue";
import { createPinia } from "@/my-pinia";
const app = createApp(App);
const pinia = createPinia()
console.log("createPinia", createPinia);
// 注册pinia
app.use(pinia);
app.mount("#app");
创建store
// stores/count.ts
// 方式1
import { reactive, computed, toRefs } from "vue";
import { defineStore } from "@/my-pinia";
// export const useCountStore = defineStore("counter", {
// state: () => ({ count: 1 }),
// getters: {
// doubleCount: (store) => store.count * 2,
// },
// actions: {
// // 同步异步都在actions中完成
// addCount() {
// this.count++;
// },
// },
// });
// 方式2
export const useCountStore = defineStore("count", () => {
const state = reactive({
count: 0,
});
const doubleCount = computed(() => state.count * 2);
const addCount = () => state.count++;
return {
...toRefs(state),
doubleCount,
addCount,
};
});
页面调用
<script setup>
import { useCountStore } from "./stores/count";
const store = useCountStore();
</script>
<template>
<div>
{{ store.count }}
<div>数量:{{ store.count }} getters:{{ store.doubleCount }}</div>
<button @click="store.addCount">增加</button>
</div>
</template>
核心实现
- 通过在app中注入一个对象pinia.state,触达每一个子组件,在定义时给state追加store。
- 在获取时通过inject将store传入组件中。
createPinia.js : 创建插件并注入
- 管理state及其下的store:
- 基于effectScope生成scope,scope管理state,state内部存放所有的store,用于批量管理其中的响应式数据(控制响应式数据是否刷新视图)。
- 每个store内部也有一个scope,管理内部的属性是否刷新视图。
- 注入pinia:通过provide将pinia注入到app上,各组件实例可以获取到该pinia
import { markRaw, effectScope, ref } from "vue";
import { symbolPinia } from "./rootStore";
export function createPinia() {
/**
* 用法
* const pinia = createPinia();app.use(pinia);
* 所以createPinia返回一个pinia插件
*
* pinia需要有的能力
* 不被响应式[markRaw]
* 能被挂载到全局上
*/
// 创建一个独立的scope,将所有的store都丢进去,后期可以统一管理store
const scope = effectScope(true);
const state = scope.run(() => ref({}));
// 创建一个静态属性(markRaw),无法被响应式[表层]
const pinia = markRaw({
install(app) {
// app.use(pinia)时,会调用pinia的install方法并传入app
// 将pinia挂载到所有组件的属性上,组件可通过inject获取
app.provide(symbolPinia, pinia);
// vue2语法
app.config.globalProperties.$pinia = pinia;
pinia._a = app;
},
_a: null, //挂载app实例,后期可能用到
_e: scope, //指向effectScope
_s: new Map(),
state,
});
return pinia;
}
defineStore.js : 创建store
- 每一个store都是一个响应式对象reactive
import {
effectScope,
getCurrentInstance,
inject,
reactive,
computed,
toRefs,
} from "vue";
import { symbolPinia } from "@/my-pinia/rootStore.js";
export function defineStore(idOrOptions, setup) {
// 整合参数 defineStore({id:'xx'}) defineStore('id',setup) defineStore('id',options)
let id;
let options;
if (typeof idOrOptions == "string") {
id = idOrOptions;
options = setup;
} else {
id = idOrOptions.id;
options = idOrOptions;
}
const isSetupStore = typeof setup === "function";
function useStore() {
// 保证useStore是在setup中执行的,只有在setup中才能通过组件实例注入父组件属性
const currentInstant = getCurrentInstance();
// 注入app中的pinia对象
const pinia = currentInstant && inject(symbolPinia);
// 判断pinia中是否有该store
if (!pinia._s.has(id)) {
// 第一次创建store
if (isSetupStore) {
createSetupStore(id, options, pinia);
} else {
createOptionStore(id, options, pinia);
}
}
// 获取pinia._s中的store并返回
const store = pinia._s.get(id);
return store;
}
return useStore;
}
createSetupStore : 根据传入的函数返回store
// 函数式store(传入一个setup函数并返回对象)
function createSetupStore(id, setup, pinia) {
// !!!这是最终挂载到state上的store,每个store都是一个响应式对象
let store = reactive({});
let scope;
/**
* 在根scope上再运行一次run,则此run也会被scope收集控制
* setupScope => setup()
*/
const setupScope = pinia._e.run(() => {
// 创建一个scope,让store本身也有停止本身收集依赖的能力
scope = effectScope();
// 内部的setup也会被pinia控制
return scope.run(() => setup());
});
// 包装action,将其this指向store
function wrapAction(name, action) {
return function () {
// 此处可以有额外逻辑,且返回值也可以经过处理
return action.apply(store, arguments);
};
}
for (let key in setupScope) {
let prop = setupScope[key];
if (typeof prop === "function") {
/**
* 切片编程:此处主要目的是修改其this指向为当前store
* 也可以加若干逻辑在其中
*/
setupScope[key] = wrapAction(key, prop);
}
}
// 覆盖store并挂载方法于store中。
Object.assign(store, setupScope);
// 挂载到pinia上
pinia._s.set(id, store);
return store;
}
createOptionStore(建议写法) :根据传入的配置返回store
- 将选项配置整合一下再调用createSetupStore
// 普通的store(state、getters...):根据id和options,创建并挂载store至pinia中
function createOptionStore(id, options, pinia) {
// 取出当前store的配置
let { state, getters, actions } = options;
let store = reactive({});
// 提供一个setup函数
function setup() {
// 将state挂载到pinia上的state中
pinia.state.value[id] = state ? state() : {};
// pinia.state => markRaw({state})
// state只被proxy,但是没有响应式,因此需要将其响应式
// 将其state返回的值变为响应式的,便于computed收集依赖
const localStore = toRefs(pinia.state.value[id]);
return Object.assign(
localStore,
actions,
// 将getters用computed缓存,并暴露到store上
Object.keys(getters).reduce((computedGetters, name) => {
// getters: {
// doubleCount: (store) => store.count * 2,
// },
computedGetters[name] = computed(() => {
// 如果此处pinia.state.value[id]不拿toRefs包裹
// 则返回的是一个具体值,computed无法收集到store中的数据变化
return getters[name].call(store, store);
}); // 改变其this,并把store传入,两种写法
return computedGetters;
}, {})
);
}
store = createSetupStore(id, setup, pinia);
return store;
}
主文件(lib/index.js)
// index.js
export { createPinia } from "./createPinia";
export { defineStore } from "./defineStore";
// rootStore.js
export const symbolPinia = Symbol();
其余方法
$patch
:批量更改store中的属性,本质上是合并对象(深拷贝)$reset
:重置store中的state为初始值(保存刚开始的值,调用时覆盖即可)$subscribe
:监听对应store中state所有的属性,当属性变化时触发回调watch(()=>store)$onAction
:监听store中的actions,当调用actions时触发回调(发布订阅,在wrapAction中发布)$dispose
:删除store(停止收集依赖,视图不根据数据更新了[effectScope.stop])