本文参加了由公众号@若川视野 发起的每周源码共读活动, 点击了解详情一起参与。
前言
pinia是Vue 官方团队推荐代替Vuex的一款轻量级状态管理库,同时支持vue2和vue3。
pinia对比vuex的优点:
- 体积小巧,压缩后体积只有不到2KB
- ts类型支持非常好。
- 扁平化,没有模块嵌套,只有 store 的概念,store 之间可以自由使用,更好的代码分割
- 去除 mutations,只有 state,getters,actions
- 支持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)
- vue通过
.use()来注册插件,所以我们提供的createPinia方法需要返回install()函数来注册pinia - pinia支持多个store,并且没有模块嵌套,所有的store都是平铺开来的,所以我们需要定义一个内部变量
_s: new Map()来统一管理这些store,大概结构就是{ user->store, counter->store } 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并返回
- 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', ()=>{...})
- 同时根据下面使用示例可以发现,我们通过执行
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,有就返回,没有就设置
- 首先获取当前vue实例
- 使用inject()方法获取之前在createPinia时注入的pinia根存储
- 检查pinia中是否存在对应id的store,没有就设置
注意:
vue在页面加载时会默认设置currentInstance=null
vue组件初始化时会调用setCurrentInstance()来设置currentInstance在
inject方法被调用时会判断下currentInstance是否存在,然后才会进行下一步的动作。
![]()
当我们定义的
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的映射表里
分析官方的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上![]()
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;
}
}
}
}
优化
对比createOptionsStore和createSetupStore发现,他们除了在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基本功能已经实现了
$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函数调用前执行 参数
- after:状态发生变化之后的回调,接受一个参数 result,它只有在actions中的被触发函数有返回值时才有值
- args actions中被触发函数的参数
- store 当前store的信息
- name actions中被触发函数的名字
- 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;
}