Pinia的使用与原理

Pinia 与 Vuex 对比

  • vuex:

    • ts兼容性不好
    • 命名空间的缺陷(只能有一个store)
    • mutation和action有区别
  • pinia:

    • 更简洁的API
    • ts兼容性更好
    • 无命名空间的缺陷(可以创建多个store)
    • 删除了mutation,统一在action中开发

使用方法

引入

typescript 复制代码
// 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

typescript 复制代码
// 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,
  };
}); 

页面调用

html 复制代码
<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
javascript 复制代码
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
javascript 复制代码
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

typescript 复制代码
// 函数式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
typescript 复制代码
// 普通的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)

javascript 复制代码
// 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])
相关推荐
奋斗的小花生11 分钟前
c++ 多态性
开发语言·c++
魔道不误砍柴功14 分钟前
Java 中如何巧妙应用 Function 让方法复用性更强
java·开发语言·python
闲晨17 分钟前
C++ 继承:代码传承的魔法棒,开启奇幻编程之旅
java·c语言·开发语言·c++·经验分享
老猿讲编程44 分钟前
一个例子来说明Ada语言的实时性支持
开发语言·ada
Chrikk2 小时前
Go-性能调优实战案例
开发语言·后端·golang
幼儿园老大*2 小时前
Go的环境搭建以及GoLand安装教程
开发语言·经验分享·后端·golang·go
canyuemanyue2 小时前
go语言连续监控事件并回调处理
开发语言·后端·golang
杜杜的man2 小时前
【go从零单排】go语言中的指针
开发语言·后端·golang
cs_dn_Jie2 小时前
钉钉 H5 微应用 手机端调试
前端·javascript·vue.js·vue·钉钉
开心工作室_kaic2 小时前
ssm068海鲜自助餐厅系统+vue(论文+源码)_kaic
前端·javascript·vue.js