手写 Pinia 小记

Vuex 和 Pinia 的区别

  • Pinia 的特点是采用 ts 进行编写的,类型提示友好,体积小。
  • 去除 mutations, 保存了 state,getters,actions(包含了同步和异步)
  • Pinia 支持 compositionAPI,同时兼顾 OptionsApi
  • Vuex 中需要使用 module 定义模块,出现嵌套的树形结构,以此 vuex 中出现命名空间的概念。取值也非常
  • Vuex 中允许程序有一个 store
  • Pinia 可以采用多个 store,store 之间可以互相调用(扁平化),不用担心命名冲突问题
new Vuex.Store({ state:{a:1}, module:{ a:{ state:{} } } })

Pinia 的基本结构及 CompositionApi 格式

本次手写的 pinia 主要包含两个函数createPiniadefineStore

这里介绍一下 createPinia 的作用

  • 创建一个 pinia 实例,然后将其挂载到当前实例上。
    • 因为 Pinia 支持 Vue2 和 Vue3 因此,有两种方法进行挂载到相应的实例上,然后最后返回一个 Pinia 实例

createStore.ts

import type { App } from "vue";
import { ref } from "vue";
import { PiniaSymbol } from "./rootState";
export function createPinia() {
  const state = ref({}); ///映射状态

  const pinia = {
    install(app: App) {
      //希望所有组件都访问,this.$pinia
      app.config.globalProperties.$pinia = pinia;
      //   使用provide和inject来访问pinia
      app.provide(PiniaSymbol, pinia);
    },
    state,
    _s: new Map(), //每个store id对应这一个store
  };
  return pinia;
}

_s 是私有属性,主要功能是映射 id->store 的关系,以便后面取出各种 store

state 作为全局状态树的一个起点或者最顶层的状态对象,作为映射状态的

对于各个仓库中state的数据都是响应式的,都借用了 vue 的ref方法

store.ts

import { computed, getCurrentInstance, inject, reactive, toRefs } from "vue";
import { PiniaSymbol } from "./rootState";

function createOptionStore(id: any, options: any, pinia: any) {
  const { state, actions, getters = {} } = options;

  function setup(store) {
    //用户提供的状态
    pinia.state.value[id] = state ? state() : {};
    const localState = toRefs(pinia.state.value[id]); //解构出去依旧是响应式

    const setupStore = Object.assign(
      localState,
      actions, //用户提供的动作
      Object.keys(getters).reduce((computeds, getterKey) => {
        computeds[getterKey] = computed(() => {
          return getters[getterKey].call(store);
        });
        return computeds;
      }, {})
    );
    return setupStore;
  }

  createSetupStore(id, setup, pinia); //options API需要将这个Api转化为
}
//setupStore 中用户提供了完整的setup方法,直接执行即可
function createSetupStore(id: any, setup: any, pinia: any) {
  const store = reactive({}); //创建响应式对象
  function wrapAction(action: Function) {
    return function () {
      //保证this对象
      return action.call(store, ...arguments);
    };
  }

  const setupStore = setup(store); //拿到的setupStore 可能没有处理过this指向
  for (let key in setupStore) {
    const value = setupStore[key];
    if (typeof value === "function") {
      setupStore[key] = wrapAction(value); //将函数的this永远指向store
    }
  }
  Object.assign(store, setupStore);
  console.log(store);
  pinia._s.set(id, store);
  return store;
}

export function defineStore(idOrOptions: string | any, setup: any) {
  let id: any;
  let options: any;

  if (typeof idOrOptions === "string") {
    id = idOrOptions;
    options = setup;
  } else {
    options = idOrOptions;
    id = idOrOptions.id;
  }
  function useStore() {
    const currentInstance = getCurrentInstance();
    const pinia: any = currentInstance && inject(PiniaSymbol);
    // return store
    //useStore只能在组件中使用
    if (!pinia._s.has(id)) {
      //第一次使用
      //创建选项store,还可能是setupStore
      if (typeof setup === "function") {
        createSetupStore(id, setup, pinia);
      } else {
        createOptionStore(id, options, pinia);
      }
    }
    const store = pinia._s.get(id);
    return store;
  }
  return useStore;
}

都知道 defineStore 返回的是一个对象,因此在useStore函数最后返回的是store对象。

因为 defineStore 支持 vue2 和 vue3 因此有两种风格,OptionsAPI 和 CompositionAPI

对于 OptionsAPI

可能出现

export const useCounterStore = defineStore("counter", {
  state: () => {
    return {
      count: 0,
    };
  },
  getters: {
    double() {
      return this.count * 2;
    },
  },
  actions: {
    increment(payload: number) {
      this.count += payload;
    },
  },
});

这种情况,或者是

export const useCounterStore = defineStore({
  id: "counter",
  state: () => {
    return {
      count: 0,
    };
  },
  getters: {
    double() {
      return this.count * 2;
    },
  },
  actions: {
    increment(payload: number) {
      this.count += payload;
    },
  },
});

对于这种选项式处理,调用的是createOptionStore 方法

createOptionStore 方法接收三个参数,当前仓库的名字,配置对象,以及 pinia 实例

createOptionsStore 最终返回的是一个对象

  • 通过调用 options.state() 初始化并将其值赋给全局 pinia 状态树中对应 id 的位置。
  • 然后通过 toRefs 方法,转化为响应式对象,相当于将 state 再包裹一层防止解构丢失响应式
  • 然后通过 setup 函数将其内容转化为函数(最终这个函数要返回一个对象),以便后续 CompositionAPI 的统一处理
export const useCounterStore = defineStore("counter", () => {
  const count = ref(0);
  const todoStore = useTodoStore();

  const double = computed(() => {
    return count.value * 2;
  });
  const increment = (payload: number) => {
    console.log(todoStore);
    count.value += payload;
  };
  return {
    count,
    double,
    increment,
  };
});

createSetupStore方法中对 state 中的属性进行处理

  • 首先创建一个响应式对象进行存储值

  • 随后调用 setup 方法,拿到一个对象,其中包含 state,getter,actions

  • 然后遍历该对象,如果得到的值是 function,那么就将其使用高阶函数处理,以防止解构后丢失 this 指针

  • 执行传入的 setup 函数获取处理后的 store 结构,并将其中的函数包装以确保正确的上下文。

  • 对于各种操作(actions)调用直接进行调用,然后放在当前仓库上

Object.assign 将 actions 对象上的方法放在 localState 上,以便可以实现xxxStore.yyy()进行调用方法

getters 本身就是计算属性,调用 getters 上的方法,然后将其用计算属性包裹得到

Object.keys(getters).reduce((computeds, getterKey) => {
  computeds[getterKey] = computed(() => {
    return getters[getterKey].call(store);
  });
  return computeds;
}, {});

这一步相当于是,创建一个新对象,然后执行 getters 上的方法,得到的数据通过 computed 包裹,后放在新对象上

然后通过 Object.assign,将这些属性拷贝到 localState 上

最终得到的是一个setupStore

最后放在 pinia._s 上