一步一步手写pinia核心功能

1,289 阅读7分钟

本文参加了由公众号@若川视野 发起的每周源码共读活动,    点击了解详情一起参与。

前言

pinia 是Vue 官方团队推荐代替Vuex的一款轻量级状态管理库,同时支持vue2和vue3。

pinia对比vuex的优点:

  1. 体积小巧,压缩后体积只有不到2KB
  2. ts类型支持非常好。
  3. 扁平化,没有模块嵌套,只有 store 的概念,store 之间可以自由使用,更好的代码分割
  4. 去除 mutations,只有 state,getters,actions
  5. 支持vue2辅助函数,devtool

基本使用

初始化一个vue3项目,并安装pinia

yarn add pinia
# 或者使用 npm
npm install pinia

在main.js中注册pinia

 //src/main.js
import { createApp } from "vue";
import { createPinia } from "pinia";
import App from "./App.vue";
createApp(App).use(createPinia()).mount("#app");

在src目录下新建store/index.js,存放我们的示例store

// src/store/index.js
import { defineStore } from "pinia";
//登录模拟
function apiLogin(a, p) {
  if (a === "ed" && p === "ed") return Promise.resolve({ isAdmin: true });
  if (p === "ed") return Promise.resolve({ isAdmin: false });
  return Promise.reject(new Error("invalid credentials"));
}
//延时
const delay = (t: number) => new Promise((r) => setTimeout(r, t))
//示例1 id+optionsStore
export const useCounterStore = defineStore("counter", {
  state: () => ({ count: 0 }),
  getters: {
    double(state) {
      return state.count * 2;
    },
  },
  actions: {
    increment(payload) {
      this.count += payload;
    },
  },
});
//示例2 optionsStore
export const useUserStore = defineStore({
  id: "user",
  state: () => ({ name: "Eduardo", isAdmin: true }),
  actions: {
    async login(user, password) {
      const userData = await apiLogin(user, password);
      this.$patch({ name: user, ...userData });
    },
    logout() {
      this.$patch({ name: "", isAdmin: false });
    },
  },
});
//示例3
export const useCounterStepStore = defineStore("counterSetup", () => {
  const state = reactive({
    n: 0,
    incrementedTimes: 0,
    decrementedTimes: 0,
    numbers: [],
  });
  const double = computed(() => state.n * 2);
  function increment(amount = 1, b = 1, c = 2) {
    if (typeof amount !== "number") {
      amount = 1;
    }
    state.incrementedTimes++;
    state.n += amount;
  }

  function changeMe() {
    console.log("change me to test HMR");
  }

  async function fail() {
    const n = state.n;
    await delay(1000);
    state.numbers.push(n);
    await delay(1000);
    if (state.n !== n) {
      throw new Error("Someone changed n!");
    }

    return n;
  }

  async function decrementToZero(interval) {
    if (state.n <= 0) return;

    while (state.n > 0) {
      state.n -= 1;
      state.decrementedTimes += 1;
      await delay(interval);
    }
  }

  return {
    ...toRefs(state),
    aa,
    double,
    increment,
    fail,
    changeMe,
    decrementToZero,
  };
});

关于基本用法,这里就不过多赘述了, pinia官方文档里有详细介绍,篇幅有限,下面就直接开始手写源码部分吧

开始实现自己的mini-pinia

mini-pinia会实现支持vue3的 actions,getters,state, $subscribe, $patch,$onAction等功能和插件

注册 createPinia

//main.js
import { createApp } from "vue";
import { createPinia } from "pinia";
const Vue=createApp(App)
const Pinia=createPinia()
Vue.use(Pinia)
  1. vue通过.use()来注册插件,所以我们提供的createPinia方法需要返回install()函数来注册pinia
  2. pinia支持多个store,并且没有模块嵌套,所有的store都是平铺开来的,所以我们需要定义一个内部变量_s: new Map()来统一管理这些store,大概结构就是{ user->store, counter->store }
  3. pinia最核心的是对状态的管理,所以还需要定义一个响应式的变量state来存储所有store的状态
//src/pinia/createPinia.js
export const piniaSymbol = Symbol("pinia");//使用symbol定义了唯一的key
export function createPinia(){
 const pinia={
     install(app) {
         /**
          * const pinia=createPinia()
          * 当使用Vue.use(pinia)的时候,Vue会检测有咩有安装过pinia这个插件,没有的话会调用
          * pinia的install方法,并把自身实例作为参数传进去 这里的app就是vue实例
          * provide和inject一起使用可以实现数据共享的能力
          *   provide把接收到的key,value挂载到vue实例的provides上
          *   inject则通过传入的key从vue实例的provides上获取value
          */ 
       app.provide(piniaSymbol, pinia);
     },
   _s: new Map(), //会收集所有的store,一方面做缓存用,同时也为了方便管理,例如:可能会卸载全部的store
   state: ref({}),//存储所有的状态
 }
 return pinia
}

defineStore

defineStore是pinia最重要的方法,主要作用是生成store并返回

  1. defineStore接收参数的方式有3种id+options、options、id+function
//id+options
defineStore('counter',{  state,getter,actions })
/**options id在options内部*/
 defineStore({id:'counter', state,getter,actions}) 
//id+function
defineStore('counter', ()=>{...}) 
  1. 同时根据下面使用示例可以发现,我们通过执行useCounterStore()拿到了store对象,推断出defineStore返回的一定是个函数,所以我们在defineStore内部定义一个useStore方法,并返回.
//src/store/index.js
export const useCounterStore = defineStore("counter", { 
    state: () => ({ count: 0 }), 
    getters: { //...省略部分代码 },
    actions: { //...省略部分代码  }, 
});
//app.vue
//......省略部分代码 
import { useCounterStore } from "./store/index";
const counterStore = useCounterStore();

开始定义defineStore

// src/pinia/store.js
export function defineStore(idOrOptions, setup){
     // 初始化变量id和options
     let id;
     let options;
     // 如果 setup是一个 function 那么就是 id + function类型的入参
     const isSetupStore = typeof (setup) === "function";
     // 如果idOrOptions是字符串类型,那么可能是id+function 或者 id + options的入参方式
     if(typeof (idOrOptions) === "string"){
         id = idOrOptions
         options = setup;//可能是function也可能是options
     } else {
         // 这种一定是options的入参方式
         id = idOrOptions.id;
         options=idOrOptions
     }
     function useStore() {
    
     }
     return useStore;
}

useStore

useStore 主要是检查当前id下有没有对应的store,有就返回,没有就设置

  1. 首先获取当前vue实例
  2. 使用inject()方法获取之前在createPinia时注入的pinia根存储
  3. 检查pinia中是否存在对应id的store,没有就设置

注意:

vue在页面加载时会默认设置currentInstance=null

vue组件初始化时会调用setCurrentInstance()来设置currentInstance

inject方法被调用时会判断下currentInstance是否存在,然后才会进行下一步的动作。

image.pngimage.png

当我们定义的store在组件内部被调用时没有问题,这时currentInstance是有值的

但是当store在组件外部被调用时,没有任何组件被初始化 currentInstance还是null这时inject就会不生效,拿不到值

//main.js
import { createApp } from "vue";
import { createPinia } from "pinia";
import App from "./App.vue";
import { useCounterStore } from ./store/index
createApp(App).use(createPinia()).mount("#app");
//这种情况下是有问题的
const store=useCounterStore()

方案: 可以在注册pinia的时候就设置一个全局变量 activePinia并导出,供其他地方使用

// createPinia.js
//...省略部分代码
export let activePinia //全局变量
export const setActivePinia = (pinia) => (activePinia = pinia);//设置当前的pinia
export function createPinia() {
  const pinia = {
    install(app) {
      setActivePinia(pinia)//注册的时候就把当前的pinia保存下来
      //...省略部分代码
    },
   //...省略部分代码
  };
  return pinia;
} 
// src/pinia/store.js
import { getCurrentInstance, inject } from "vue";
import { piniaSymbol, activePinia, setActivePinia } from "./createPinia";
function  createOptionsStore(id, options, pinia){
}
export function defineStore(idOrOptions, setup) {
 //初始化变量id和options
  let id, options;
 //...省略部分代码
 function useStore(){
    let pinia = getCurrentInstance() && inject(piniaSymbol);
    //如果可以拿到实例并且可以拿到注册的pinia,就再设置一下,反正这样总没错
    if (pinia) setActivePinia(pinia);
    //这时pinia一定有值了
    pinia = activePinia;
    //如果pinia的映射表中找不到对应id下的store,就设置
    if (!pinia._s.has(id)) {
      if (!isSetupStore) {
        //先处理options是对象的情况
        createOptionsStore(id, options, pinia);
      }
    }
    //返回pinia映射表中对应id下的store
    const store = pinia._s.get(id);
    return store;
 }
 return useStore
}

createOptionsStore 内部会拿到用户的选项将它变成setup语法

createOptionsStore的作用就是创建一个store并设置到pinia对应id的映射表里 image.png

分析官方的pinia插件中的store可以看到,state,getters,和actions都是解构后平铺到store上的,并且state中count是个响应式对象,getters中的double是计算属性,所以定义一个setup函数对用户传入的options做处理

注意

getters中的属性都是函数,getters: { double(state) { return this.count * 2; } }所以需要把它转成计算属性,并且需要用call方法解决它可能存在的this指向问题 同样的actions中的方法也可能存在this指向问题,也需要处理下

function createOptionsStore(id,options,pinia){
  const { state, getters, actions } = options;
  const store=reactive({})//store就是个响应式对象, 后续一些不是用户定义的属性和方法,内置的pai会增加到这个store上
  //options中的 state, getters, actions 不能直接拿来用,需要做处理
  function setup(){//这里处理用户传入的options
  //options中的state是个函数,所以需要执行后,挂载到之前pinia声明的state对应的id下,并且把它设置为响应式
    pinia.state.value[id] = state?.() || {};
    const localState = toRefs(pinia.state.value[id]);
    return Object.assign(
        localState,  //用户的状态
        actions, //用户的动作
        Object.keys(getters || {}).reduce((computedGetters, name) => {
        /**
         * 这里需要把getters中的属性转为计算属性,并且把this指向固定为store,因为可能会有
         * getters: { double(state) {  return this.count * 2;  }  },这样的用法
         */
        computedGetters[name] = computed(() => {
          const store = pinia._s.get(id);
          return getters[name].call(store, store);
        });
        return computedGetters;
      }, {})
  }
    const steupStore=setup()//处理之后的option
    function wrapAction(name, action) {
        return function () {
          let ret = action.apply(store, arguments);
          return ret;
        };
    }
  /**
   * actions中的方法也可能存在this指向问题
   *    App.vue中
   *    import {  useCounterStore } from "./store/counter";
   *     
   *    场景 1.   const counterStore = useCounterStore();
   *             counterStore.increment() 这时increment中的方法this指向counterStore没有问题
   *    场景 2.   const {increment} = useCounterStore();
   *             increment()这时increment中的this为undefined
   *    所以需要遍历actions中的每个属性,定义个函数wrapAction把他包起来,并设置this指向为当前的store
   */
  for (let key in setupStore) {
    const prop = setupStore[key];
    if (typeof (prop) === "function") {
      setupStore[key] = wrapAction(key, prop);
    }
  }
  //这里合并用户传入的setupStore(处理后的options)和store本身自带的一些方法($patch,$reset...)
  Object.assign(store,setupStore) 
  pinia._s.set(id, store);//将store和id映射起来
  return store
}

createSetupStore 核心方法

所有的store都是通过这个方法来创建的

上面的代码对optionsStore进行了处理,接下来分析下用户传入setupStore(id+function)的场景,对比其中function的返回值和上面createOptionsStore中的setup方法的返回值,可以发现,setupStore()执行后就已经是我们需要的构建的对象,只要再对action做处理就可以了

注意: 在 optionsStore (id+options) 中由于结构是固定的,我们很明确的知道state就是它的状态,所以我们就可以把它存到pinia的state上

image.png
export const useCounterStore = defineStore("counter", { 
   state: () => ({ count: 0 }), //count属于state
   getters: {}, //...省略部分代码  
   actions: {}  //...省略部分代码  
})

但是在setupStore中就不容易区分那么哪些属性是state的,因为最终我们需要把属于状态的存到pinia.state.value[id]上嘛

// src/store/index.js
//id+function
defineStore('counter', ()=>{...}); 
//示例 counterSetupStore
export const useCounterStepStore = defineStore("counterSetup", () => {
 const count= ref(10)
 const double = computed(() => count.value * 2);
 function increment(amount){
   count += amount;
 }
 //这些值无法确定哪个是状态
 return { count,double,increment}; 
});

方案:对setupStore返回值做一下处理,看一下谁是状态,然后把它存到pinia的状态里去

//createSetupStore
const isComputed = (v) => !!(isRef(v) && v.effect); //计算属性是ref同时也是effect
function createSetupStore(id, setup, pinia) {
  const store = reactive({});
  const initialState = pinia.state.value[id]//对于setup而言是没有初始化过状态,这里的值是undefined
  //如果没有先默认给个空状态
  if(!initialState){
      pinia.state.value[id]={}
  }
  const setupStore = setup();
  
  function wrapAction(name, action) {
    return function () {
      let ret = action.apply(store, arguments);
      return ret;
    };
  }
 for(let key in setupStore) {
     const prop = setupStore[key];
     if(typeof(prop) ==='function'){
       setupStore[key] = wrapAction(key, prop);
     }
     //如果prop是普通对象或是响应式的就把它存到pinia.state.value[id]上,isRef,isReactive是vue3提供的api,
    if ((isRef(prop) && !isComputed(prop)) || isReactive(prop)) {
      if (!isOption) {
        pinia.state.value[id][key] = prop;
      }
    }
 }
}

优化

对比createOptionsStorecreateSetupStore发现,他们除了在setupStore的处理上不太一样外,其他地方都差不多,可以共用,优化后的defineStore如下

import { getCurrentInstance, inject, reactive, toRefs, computed } from "vue";
import { piniaSymbol, activePinia, setActivePinia } from "./createPinia";
const isComputed = (v) => !!(isRef(v) && v.effect); //计算属性是ref同时也是effect
function createOptionsStore(id, options, pinia) {
  const { state, getters, actions } = options;
  //options中的 state, getters, actions 不能直接拿来用,需要做处理
  function setup() {
    //options中的state是个函数,所以需要执行后,挂载到之前pinia声明的state对应的id下,并且把它设置为响应式的
    pinia.state.value[id] = state?.() || {};
    const localState = toRefs(pinia.state.value[id]);

    return Object.assign(
      localState,
      actions,
      Object.keys(getters || {}).reduce((computedGetters, name) => {
        /**
         * 这里需要把getters中的属性转为计算属性,并且把this指向固定为store,因为可能会有
         * getters: { double(state) {  return this.count * 2;  }  },这样的用法
         */
        computedGetters[name] = computed(() => {
          const store = pinia._s.get(id);
          return getters[name].call(store, store);
        });
        return computedGetters;
      }, {})
    );
  }
  createSetupStore(id, setup, pinia,true);
}
//isOption用来判断是不是options对象,如果不是需要对setup的返回值做处理
function createSetupStore(id, setup, pinia, isOption) {
  //后续一些不是用户定义的属性和方法,内置的api会增加到这个store上
  const store = reactive({});
  //对于setup而言是没有初始化过状态,这里的值是undefined
  const initialState = pinia.state.value[id];
  //如果没有先默认给个空状态,
  if (!initialState && !isOption) {
    pinia.state.value[id] = {};
  }
  const setupStore = setup();
  function wrapAction(name, action) {
    return function () {
      let ret = action.apply(store, arguments);
      return ret;
    };
  }
   /**
   * actions中的方法也可能存在this指向问题
   *    App.vue中
   *    import {  useCounterStore } from "./store/counter";
   *     
   *    场景 1.  const counterStore = useCounterStore();
                counterStore.increment() 这时increment中的方法this指向counterStore没有问题
        场景2.   const {increment} = useCounterStore();
                increment()这时this为undefined
   *    所以需要遍历actions中的每个属性,在外面给他包一层,并设置this指向为当前的store
   */
   for (let key in setupStore) {
     const prop = setupStore[key]
     if (typeof(prop) === "function") {
       setupStore[key] = wrapAction(key, prop)
     }
    //如果prop是普通对象或是响应式的就把它存到pinia.state.value[id]上,isRef,isReactive是vue3提供的api,
     if ((isRef(prop) && !isComputed(prop)) || isReactive(prop)) {
      if (!isOption) {
        pinia.state.value[id][key] = prop;
      }
    }
   }
  //把 setupStore(options处理后的) 和store合并
  Object.assign(store, setupStore);
  pinia._s.set(id, store); //将store和id映射起来
  return store;
}
export function defineStore(idOrOptions, setup) {
  //初始化变量id和options
  let id
  let options
  //如果 setup是一个 function 那么就是 id + function类型的入参
  const isSetupStore = typeof setup === "function";
  // 如果idOrOptions是字符串类型,那么可能是id+function 或者 id + options的入参方式
  if (typeof idOrOptions === "string") {
      id = idOrOptions
      options = setup
  } else {
      //这种一定是options的入参
      id = idOrOptions.id
      options = idOrOptions
  } 
  function useStore() {
    let pinia = getCurrentInstance() && inject(piniaSymbol);
    //如果可以拿到实例并且可以拿到注册的pinia,就再设置一下,反正这样总没错
    if (pinia) setActivePinia(pinia)
    //这时pinia一定有值了
    pinia = activePinia
    //如果pinia的映射表中找不到id对应的store,需要设置一下
    if (!pinia._s.has(id)) {
      // optionsStore
      if (!isSetupStore) {
        createOptionsStore(id, options, pinia);
      } else {
        //setupStore
        createSetupStore(id, setup, pinia);
      }
    }
    const store = pinia._s.get(id);
    return store;
  }
  return useStore;
}

至此,我们的mini-pinia 的 actions,state,getters基本功能已经实现了

tutieshi_366x196_8s.gif

$patch

$patch的作用是批量更新多个属性,举个例子:

//app.vue
import { useCounterStore } from "./store/counter";
const store=useCounterStore()
const add=()=>{
  //可能会在一个方法里,多次修改store里的属性,那么会导致插件监控到多次更新,所以pinia提供了一次新修改多个属性的方法
  store.count+=1
  store.n+=1
}
//$patch的用法,一次修改多个属性,参数可以是对象也可以是一个方法
const add1=()=>store.$patch({count:2,n:2})
const add2=()=>store.$patch((state)=>{
  state.count = 1000 
})

$patch的原理很简单,如果入参是对象就把要修改的部分属性和原属性做合并,如果参数是方法,直接执行就可以

实现

//createSetupStore
const isObject = (value) => typeof value === "object" && value !== null;
function mergeReactiveObject(target, state) {
  for (let key in state) {
    let oldValue = target[key];//pinia中的state
    let newValue = state[key];//要修改的state
    //如果两个都是对象需要递归合并
    if (isObject(oldValue) && isObject(newValue)) {
      target[key] = mergeReactiveObject(oldValue, newValue);
    } else {
      target[key] = newValue;
    }
  }
  return target;
}
function createSetupStore(id, setup, pinia, isOption) {
 function $patch(partialStateOrMutation){
  //参数是对象,合并
  if (typeof(partialStateOrMutation) === "object") {
      mergeReactiveObject(pinia.state.value[id], partialStateOrMutation)
    } else {
    //参数是方法,执行就可以了
      partialStateOrMutation(pinia.state.value[id])
    }
 }
 //之前有定义了一个空的store对象,用来存放内置api的,这里就可以用上了
 const store = reactive({
   $patch
  });
 //...省略部分代码
}

$reset

$reset的作用是重置为默认状态,并且options类型的store才支持

function createOptionsStore(id, options, pinia) {
  const { state, getters, actions } = options;
    //...省略部分代码
  const store = createSetupStore(id, setup, pinia, true);
  store.$reset = function () {
    //options类型的store中state一定是个函数
    const newState = state ? state() : {}
    store.$patch(($state) => {
      //这里的$state指的是pinia.state.value[id]
      Object.assign($state, newState);
    });
  };
}

$subscribe

监听状态变化,只要状态变化就执行回调函数

//$subscribe用法
import { useCounterStore } from "./store/counter";
const store= useCounterStore()
store.$subscribe((storeInfo,state)=>{
 //storeInfo 当前store信息,包含storeId...
 //state 新状态
})

原理很简单,就是利用了vue3提供的watch,监听状态变化,然后执行回调函数

实现$subscribe

import { watch } from "vue";
function createSetupStore(id, setup, pinia, isOption) {
    function $patch(partialStateOrMutation) {
    //...省略部分代码
    }
    const store = reactive({
        $patch,
        $subscribe(callback) {
          //监听pinia中当前id下状态的变化,调用callback
          watch(pinia.state.value[id], (state) => {
            callback({ storeId: id }, state);
          });
        },
      });
}

$onAction

监听用户调用action的方法 该回调函数内部的代码会先于actions函数调用前执行 参数

  1. after:状态发生变化之后的回调,接受一个参数 result,它只有在actions中的被触发函数有返回值时才有值
  2. args actions中被触发函数的参数
  3. store 当前store的信息
  4. name actions中被触发函数的名字
  5. onError 捕获错误的回调
//使用示例
import { useCounterStore } from "./store/counter";
const store= useCounterStore()
store.$onAction(({ after, args, name, onError, store }) => {
    //after 状态发生变化之后的回调
    //onError 捕获错误的回调
    //name actions中被触发函数的名字
    //args actions中被触发函数的参数
    //store 当前store的信息
    console.log('actions中的函数被触发了')//这个发生在状态变化之前
    onError(error=>{
      console.log(error)
    })
    after((result) => {
      //当actions中的函数有返回值时,可以接收到result
      console.log('状态变化后', result);
    });
});

实现原理: 发布订阅模式

初始化存放订阅函数的列表 const subscriptions = [];

当$onAction被触发时,通过addSubscription把回调函数放到subscriptions

在actions中的方法被触发时,循环执行subscriptions中的回调函数,可以放在wrapAction里

同样的,其中的after和onError也是发布订阅模式

// src/pinia/store.js
//把回调存到数组中
function addSubscription(subscriptions, callback) {
  subscriptions.push(callback);
}
//循环回调函数列表并执行
function triggerSubscribe(subscriptions, ...args) {
  subscriptions.slice().forEach((callback) => {
    callback(...args);
  });
}
function createSetupStore(id, setup, pinia, isOption) {
     const subscriptions = [];//订阅回调函数的列表
     function $patch(partialStateOrMutation) {}//...省略部分代码
     const store = reactive({
        $patch,
        $subscribe(callback) {},//...省略部分代码
        $onAction: addSubscription.bind(null, subscriptions),//把回调存到数组中
     });
    function wrapAction(name, action) {
       return function () {
          const afterCallbackList = [];
          const onErrorCallbackList = [];
          //当给after传递回调的时候,把它存到afterCallbackList
          function after(callback) {
            afterCallbackList.push(callback);
          }
           //当给onError传递回调的时候,把它存到onErrorCallbackList
          function onError(callback) {
            onErrorCallbackList.push(callback);
          }
          triggerSubscribe(subscriptions, {
            args: Array.from(arguments),
            name,
            store,
          });
           let ret
           try{
              ret = action.apply(store, arguments);
           }catch(e){
             //触发action时可能会报错,需要触发错误的回调
             triggerSubscribe(onErrorCallbackList,e)
           }
           //如果返回值是Promise
           if (ret instanceof Promise) {
            return ret
              .then((value) => {
                //成功,调用after回调
                triggerSubscribe(afterCallbackList, value);
                return value;
              })
              .catch((e) => {
                //失败,调用onError回调
                triggerSubscribe(onErrorCallbackList, e);
                return Promise.reject(e);
              });
           }
           //不是promise也需要处理下
           triggerSubscriptions(afterCallbackList, ret);
           return ret;
        };
    }
    //...省略部分代码
}

插件

pinia支持用户通过pinia.use(()=>{})的方法注册自己的逻辑, 插件就是个函数,use是用来注册插件的, 我们统一的功能就可以写在这里了,比如说要做一些持久化的功能: use给回调函数传递的参数有 app:vue实例 pinia: pinia的实例 store: 当前store的信息 options

import { createApp } from "vue";
import { createPinia } from "@/MyPinia";
import App from "./App.vue";
const Vue = createApp(App);
const pinia = createPinia();
 /**
 * 定义多个store会执行多次,
 * 参数:
 *  app:vue实例
 *  pinia实例
 *  store 当前store的信息 最重要的
 */
 //每次状态变化都把数据存到localstorage中
 pinia.use(function({store}){
   store.$subscribe((store, state) => {
    localStorage.setItem(`${store.storeId}-PINIA_STATE`, JSON.stringify(state));
  })
})
Vue.use(pinia).mount("#app");

实现插件

首先在createPinia中创建use

初始化_p=[]用来存储use中的回调函数,每当use被调用时都把回调放到_p里

在创建store的地方循环执行_p里的回调函数

//createPinia
import { ref } from "vue";
export const piniaSymbol = Symbol("pinia");
export let activePinia;
export const setActivePinia = (pinia) => (activePinia = pinia);
export function createPinia() {
 const _p = [];//用来存储插件
 const pinia = {
   use(plugin) {
     _p.push(plugin);
     return this; //为了链式调用,返回this
   },
   _p,
   install(app) {
     app.provide(piniaSymbol, this);
     app.config.globalProperties.$pinia = pinia;
   },
   _s: new Map(),
   state: ref({}),
 };
 return pinia;
}
//store.js
//createSetupStore
function createSetupStore(id, setup, pinia, isOption) {
     //...省略部分代码
    const store = reactive({
     //...省略部分代码
    });
     //拿到插件列表执行
    pinia._p.forEach((plugin) => {
       plugin({ store });
    });
    //...省略部分代码
   return store;
}

完结

到这里 pinia的大部分功能已经实现了,完整代码如下

 //src/pinia/createPinia.js
import { ref } from "vue";
export const piniaSymbol = Symbol("pinia");
export let activePinia; //全局变量
export const setActivePinia = (pinia) => (activePinia = pinia); //设置当前的pinia
export function createPinia() {
  const _p = [];//存放插件
  const pinia = {
    use(plugin) {
      _p.push(plugin);
      return this; //为了链式调用,返回this
    },
    _p,
    install(app) {
      setActivePinia(pinia); //注册的时候就把当前的pinia保存下来
      /**
       * const pinia=createPinia()
       * 当使用Vue.use(pinia)的时候,Vue会检测有咩有安装过pinia这个插件,没有的话会调用
       * pinia的install方法,并把自身实例作为参数传进去 这里的app就是vue实例
       * provide和inject一起使用可以实现数据共享的能力
       *   provide把接收到的key,value挂载到vue实例的provides上
       *   inject则通过传入的key从vue实例的provides上获取value
       */
      app.provide(piniaSymbol, this);
      app.config.globalProperties.$pinia = pinia;
    },
    _s: new Map(), //会收集所有的store,一方面做缓存用,同时也为了方便管理,例如:可能会卸载全部的
    state: ref({}), //存储所有的状态
  };
  return pinia;
}
 //src/pinia/store.js
 import {
  getCurrentInstance,
  inject,
  reactive,
  toRefs,
  computed,
  isRef,
  isReactive,
  watch,
} from "vue";
import { piniaSymbol, activePinia, setActivePinia } from "./createPinia";
const isComputed = (v) => !!(isRef(v) && v.effect); //计算属性是ref同时也是effect
const isObject = (value) => typeof value === "object" && value !== null;
function addSubscription(subscriptions, callback) {
  subscriptions.push(callback);
}
function triggerSubscribe(subscriptions, ...args) {
  subscriptions.slice().forEach((callback) => {
    callback(...args);
  });
}
function mergeReactiveObject(target, state) {
  for (let key in state) {
    let oldValue = target[key]; //pinia中的state
    let newValue = state[key]; //要修改的state
    //如果两个都是对象需要递归合并
    if (isObject(oldValue) && isObject(newValue)) {
      target[key] = mergeReactiveObject(oldValue, newValue);
    } else {
      target[key] = newValue;
    }
  }
  return target;
}
function createOptionsStore(id, options, pinia) {
  const { state, getters, actions } = options;
  //options中的 state, getters, actions 不能直接拿来用,需要做处理
  function setup() {
    //options中的state是个函数,所以需要执行后,挂载到之前pinia声明的state对应的id下,并且把它设置为响应式的
    pinia.state.value[id] = state?.() || {};
    const localState = toRefs(pinia.state.value[id]);
    return Object.assign(
      localState,
      actions,
      Object.keys(getters || {}).reduce((computedGetters, name) => {
        /**
         * 这里需要把getters中的属性转为计算属性,并且把this指向固定为store,因为可能会有
         * getters: { double(state) {  return this.count * 2;  }  },这样的用法
         */
        computedGetters[name] = computed(() => {
          const store = pinia._s.get(id);
          return getters[name].call(store, store);
        });
        return computedGetters;
      }, {})
    );
  }
  const store = createSetupStore(id, setup, pinia, true);
  store.$reset = function () {
    //因为只考虑optionsAPi所以state一定是个函数
    const newState = state ? state() : {};
    store.$patch(($state) => {
      Object.assign($state, newState);
    });
  };
}
function createSetupStore(id, setup, pinia, isOption) {
  const subscriptions = [];
  function $patch(partialStateOrMutation) {
    //参数是对象,合并
    if (typeof partialStateOrMutation === "object") {
      mergeReactiveObject(pinia.state.value[id], partialStateOrMutation);
    } else {
      //参数是方法,执行就可以了
      partialStateOrMutation(pinia.state.value[id]);
    }
  }
  //后续一些不是用户定义的属性和方法,内置的api会增加到这个store上
  const store = reactive({
    $patch,
    $subscribe(callback) {
      watch(pinia.state.value[id], (state) => {
        callback({ storeId: id }, state);
      });
    },
    $onAction: addSubscription.bind(null, subscriptions),
  });
  const initialState = pinia.state.value[id];
  if (!initialState && !isOption) {
    pinia.state.value[id] = {};
  }
  const setupStore = setup();
  function wrapAction(name, action) {
    return function () {
      const afterCallbackList = [];
      const onErrorCallbackList = [];
      //当给after传递回调的时候,把它存到afterCallbackList
      function after(callback) {
        afterCallbackList.push(callback);
      }
      //当给onError传递回调的时候,把它存到onErrorCallbackList
      function onError(callback) {
        onErrorCallbackList.push(callback);
      }
      triggerSubscribe(subscriptions, {
        args: Array.from(arguments),
        name,
        store,
        after,
        onError,
      });
      let ret;
      try {
        ret = action.apply(store, arguments);
      } catch (e) {
        triggerSubscribe(onErrorCallbackList, e);
      }
      if (ret instanceof Promise) {
        return ret
          .then((value) => {
            //成功,调用after回调
            triggerSubscribe(afterCallbackList, value);
            return value;
          })
          .catch((e) => {
            //失败,调用onError回调
            triggerSubscribe(onErrorCallbackList, e);
            return Promise.reject(e);
          });
      }
      triggerSubscribe(afterCallbackList, ret);
      return ret;
    };
  }
  /**
   * actions中的方法也可能存在this指向问题
   *    App.vue中
   *    import {  useCounterStore } from "./store/counter";
   *     
   *    场景 1.  const counterStore = useCounterStore();
                counterStore.increment() 这时increment中的方法this指向counterStore没有问题
        场景2.   const {increment} = useCounterStore();
                increment()这时this为undefined
   *    所以需要遍历actions中的每个属性,在外面给他包一层,并设置this指向为当前的store
   */
  for (let key in setupStore) {
    const prop = setupStore[key];
    if (typeof prop === "function") {
      setupStore[key] = wrapAction(key, prop);
    }
    //如果prop是普通对象或是响应式的就把它存到pinia.state.value[id]上
    if ((isRef(prop) && !isComputed(prop)) || isReactive(prop)) {
      if (!isOption) {
        pinia.state.value[id][key] = prop;
      }
    }
  }
  //把 setupStore(options处理后的) 和store合并
  Object.assign(store, setupStore);
  pinia._s.set(id, store); //将store和id映射起来
  console.log(pinia._p,'pinia._p');
  pinia._p.forEach((plugin) => {
    plugin({ store });
  });
  return store;
}
export function defineStore(idOrOptions, setup) {
  //初始化变量id和options
  let id;
  let options;
  //如果 setup是一个 function 那么就是 id + function类型的入参
  const isSetupStore = typeof setup === "function";
  // 如果idOrOptions是字符串类型,那么可能是id+function 或者 id + options的入参方式
  if (typeof idOrOptions === "string") {
    id = idOrOptions;
    options = setup;
  } else {
    //这种一定是options的入参
    id = idOrOptions.id;
    options = idOrOptions;
  }
  function useStore() {
    let pinia = getCurrentInstance() && inject(piniaSymbol);
    //如果可以拿到实例并且可以拿到注册的pinia,就再设置一下,反正这样总没错
    if (pinia) setActivePinia(pinia);
    //这时pinia一定有值了
    pinia = activePinia;
    //如果pinia的映射表中找不到id对应的store,需要设置一下
    if (!pinia._s.has(id)) {
      // optionsStore
      if (!isSetupStore) {
        createOptionsStore(id, options, pinia);
      } else {
        //setupStore
        createSetupStore(id, setup, pinia);
      }
    }
    const store = pinia._s.get(id);
    return store;
  }
  return useStore;
}