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++;
// },
// },
// });// 方式2export 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都丢进去,后期可以统一管理storeconst 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中是否有该storeif (!pinia._s.has(id)) {// 第一次创建storeif (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指向storefunction 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])