手写mini-pinia

399 阅读2分钟
  • pinia 是由 vue 团队开发的,适用于 vue2 和 vue3 的状态管理库。
  • 与 vue2 和 vue3 配套的状态管理库为 vuex3 和 vuex4,pinia被誉为 vuex5。
  • pinia 没有命名空间模块;
  • pinia 无需动态添加(底层通过 getCurrentInstance 获取当前 vue 实例进行关联);
  • pinia 是平面结构(利于解构),没有嵌套,可以任意交叉组合。

学习本篇文章需要有pinia的使用有一定了解,推荐一篇pinia学习教程:大菠萝?Pinia已经来了,再不学你就out了

1.Pinia源码剖析

1.1.创建Pinia实例(createPinia)

在 main.js 文件中通过 createPinia 方法定义根 pinia 实例,以下就是createPinia函数源码

// packages/pinia/src/createPinia.ts 10行

export function createPinia(): Pinia {
  const scope = effectScope(true)
  const state = scope.run<Ref<Record<string, StateTree>>>(() =>
    ref<Record<string, StateTree>>({})
  )!

  let _p: Pinia['_p'] = []
  let toBeInstalled: PiniaPlugin[] = []

  const pinia: Pinia = markRaw({
    install(app: App) { // vue插件,所以实现了install方法
      setActivePinia(pinia)
      if (!isVue2) { //vue3
        pinia._a = app // 在pinia中保存app实例
        app.provide(piniaSymbol, pinia) // 在全局提供pinia,后续在defineStore中通过inject获取pinia
        app.config.globalProperties.$pinia = pinia
        if (USE_DEVTOOLS) { // 初始化pinia的插件(无需关心)
          registerPiniaDevtools(app, pinia)
        }
        toBeInstalled.forEach((plugin) => _p.push(plugin))
        toBeInstalled = []
      }
    },

    use(plugin) {...},

    _p, // pinia插件
    _a: null, // app实例
    _e: scope, // pinia实例的effect scope
    _s: new Map<string, StoreGeneric>(), // 将子模块store,通过map统一管理(id与store映射)
    state, // 全部store中的state
  })
  if (USE_DEVTOOLS && typeof Proxy !== 'undefined') {
    pinia.use(devtoolsPlugin)
  }

  return pinia
}

1.2 定义Store(defineStore)

全局创建并注册 pinia 实例后,接下来我们可以定义需要全局管理状态的 store。定义 store 需要通过 defineStore 方法,defineStore 方法有四种传参方式

// packages/pinia/src/store.ts 802行

export function defineStore<...>( // 1
  id: Id,
  options: Omit<DefineStoreOptions<Id, S, G, A>, 'id'>
)

export function defineStore<...>( // 2
  options: DefineStoreOptions<Id, S, G, A>): StoreDefinition<Id, S, G, A>

export function defineStore<...>( // 3
  id: Id,
  storeSetup: () => SS,
  options?: DefineSetupStoreOptions<...>
)
export function defineStore( // 4
  idOrOptions: any,
  setup?: any,
  setupOptions?: any
)

defineStore 方法首先根据传入参数,判断是 options 定义还是 setup 定义,然后定义内部函数 useStore 并返回

// packages/pinia/src/store.ts 854行

  const isSetupStore = typeof setup === 'function' // 根据传入的参数判断是否是setupStore
  if (typeof idOrOptions === 'string') {...
  } else {...
  }

  function useStore(pinia?: Pinia | null, hot?: StoreGeneric): StoreGeneric {
    const currentInstance = getCurrentInstance()
    pinia =
      (__TEST__ && activePinia && activePinia._testing ? null : pinia) ||
      (currentInstance && inject(piniaSymbol, null)) // 获取pinia实例
    if (pinia) setActivePinia(pinia)

    if (__DEV__ && !activePinia) {...}

    pinia = activePinia!

    if (!pinia._s.has(id)) { // 对未注册的store进行注册
      // creating the store registers it in `pinia._s`
      if (isSetupStore) {
        createSetupStore(id, setup, options, pinia) // 如果是函数就走createSetupStore
      } else {
        createOptionsStore(id, options as any, pinia) // 如果是对象就走createOptionStore
      }

在 option 类型的定义方法 createOptionsStore 中,定义了一个 setup 方法,并将相关参数传入了 createSetupStore 方法创建一个 store 并返回,所以创建 store 的核心方式还是通过 createSetupStore 方法

//  packages/pinia/src/store.ts 131行

function createOptionsStore(id, options, pinia, hot) {
    const { state, actions, getters } = options;
    const initialState = pinia.state.value[id];
    let store;
    function setup() {
        if (!initialState && (!(process.env.NODE_ENV !== 'production') || !hot)) {...}
            else {...}
        }
        const localState = (process.env.NODE_ENV !== 'production') && hot
            ? 
              toRefs(ref(state ? state() : {}).value)
            : toRefs(pinia.state.value[id]); // 将state转成响应式的
        // 将state、getters、actions合并到一个对象中返回。保持和setup一致,以便后续统一处理。
        return assign(localState, actions, Object.keys(getters || {}).reduce((computedGetters, name) => {
            if ((process.env.NODE_ENV !== 'production') && name in localState) {
                console.warn(`[🍍]: A getter cannot have the same name as another state property. Rename one of them. Found with "${name}" in store "${id}".`);
            }
            computedGetters[name] = markRaw(computed(() => { // getters用computed进行处理
                setActivePinia(pinia);
                const store = pinia._s.get(id);
      
                if (isVue2 && !store._r)
                    return;
        
                return getters[name].call(store, store);
            }));
            return computedGetters;
        }, {}));
    }
    store = createSetupStore(id, setup, options, pinia, hot, true); // 最后还是走createSetupStore
    return store;
}

2.手写Pinia

2.1 createPina实现

import { markRaw } from "vue";
export const piniaSymbol = Symbol("pinia");

export function createPinia() {
  const pinia = markRaw({
    // 源码中也是用markRaw,将一个对象标记为不可被转为代理。返回该对象本身
    install(app) {
      pinia._a = app;
      app.provide(piniaSymbol, pinia); // 将 pinia 实例注册到全局,便于所有子组件调用
      app.config.globalProperties.$pinia = pinia;
    },
    _s: new Map(), // 存放所有子模块(单例模式)
  });
  return pinia;
}

2.2 defineStore实现

这边就不根据传入的参数类型不同去分别实现createOptionsStore和createSeupStore了,同学们感兴趣可以去实现。

import { reactive, computed, toRefs, inject, getCurrentInstance } from "vue";
import { piniaSymbol } from "./createPinia";

export function defineStore(id, options) {
  const { state: stateFn, getters, actions } = options;

  function useStore() {
    // 获取组件实例
    const currentInstance = getCurrentInstance();
    // 获取pinia
    const pinia = currentInstance && inject(piniaSymbol);

    // 第一次使用时,将store set到pinia中
    if (!pinia._s.has(id)) {
      // 处理state(stateFn: () => {count: 0} -> state: {count: 0})
      const state = reactive(stateFn());

      // 处理getters
      const getterComputed = Object.keys(getters || {}).reduce(
        (getterComputed, getterName) => {
          // 将getter通过computend进行包裹
          getterComputed[getterName] = computed(() => {
            console.log(store);
            return getters[getterName].call(store, state); // 改变this指向store,下同
          });
          return getterComputed;
        },
        {}
      );
      
      // 处理actions
      const actionFns = Object.keys(actions || {}).reduce(
        (actionFns, actionName) => {
          actionFns[actionName] = () => actions[actionName].call(store);
          return actionFns;
        },
        {}
      );

      pinia._s.set(
        id,
        reactive({
          ...toRefs(state), // 确保state是响应式
          ...getterComputed,
          ...actionFns,
        })
      );
    }
    const store = pinia._s.get(id);
    return store;
  }
  return useStore;
}

3.总结

实现pinia的核心思路如下

  1. 注册pinia插件时,通过provide将pinia实例注册到全局,在之后defineStore中通过inject获取pinia实例。
  2. 在pinia中获取_s(用于储存 id 和 store 实例的 map 映射,避免重复创建)。
  3. 将state通过reactive处理成响应式,将getters通过computed进行包裹,并且和actions通过call将this指向绑定为store。