手撕pinia源码
目录
前期准备
安装vue
根据vue官网快速搭建一个项目,删掉不必要的文件,以免影响我们开发,只保留最简洁的功能页面即可。
创建pinia
在/src
目录下创建pinia
目录,并新建一个入口文件index.js
。新建createPinia.js
文件,此文件提供createPinia
方法是插件在vue中注册的方法。新建store.js
文件,此文件提供defineStore
方法,用于定义一个store
仓库的方法。
js
// createPinia.js
export function createPinia() {
}
js
// store.js
export function defineStore() {
}
js
// index.js
import { createPinia } from "./createPinia.js";
import { defineStore } from './store';
export {
createPinia,
defineStore
}
这就是pinia
的基本结构,index.js
直接导出createPinia、defineStore
方法供外部使用即可。
接下来修改main.js
注册pinia
插件,报错没关系,因为我们还没有实现createPinia
方法。
js
// main.js
import './assets/main.css'
import { createApp } from 'vue'
import { createPinia } from '@/pinia'
import App from './App.vue'
const app = createApp(App)
app.use(createPinia())
app.mount('#app')
最终的项目目录结构如下:
text
.
├── README.md
├── index.html
├── jsconfig.json
├── package.json
├── src
│ ├── App.vue
│ ├── assets
│ │ ├── base.css
│ │ ├── logo.svg
│ │ └── main.css
│ ├── main.js
│ ├── pinia
│ │ ├── createPinia.js
│ │ ├── index.js
│ │ └── store.js
└── vite.config.js
实现createPinia方法
vue
插件规定必须实现一个install
方法,install
方法能够获取到当前的app
实例,因此我们修改一下createPinia.js
js
// createPinia.js
export function createPinia() {
const pinia = {
install(app) {
}
}
return pinia;
}
源码分析
pinia
中实例总是全局的,所以我们需要给app
挂载全的pinia
实例。- 源码中
_s
,pinia
支持多仓库,而他的仓库存储的数据结构大致为{id1: store1, id2: store2}
这种形式,因此我们需要创建一个对象存储所有的仓库。 - 源码中
_e
是一个effectScope
的vue
API,官方解释effectScope,它可以收集所有的副作用函数
通过调用stop
来让所有的响应式数据停止响应,后续我们再详细介绍它是干什么用的,而副作用函数
可谓是vue
响应式的核心,可以理解为如果关闭了副作用函数
那么数据变化,视图也不会变化。 - 源码中
state
是一个ref
的响应式对象,其主要用于存储所有store
的状态值。
编写代码
js
// createPinia.js
import { effectScope, ref } from 'vue';
import { piniaSymbol } from './rootStore';
export function createPinia() {
const scope = effectScope();
const state = scope.run(() => ref({})); // 用于存储每个store的state方法
const pinia = {
_s: new Map(), // 用来存储所有的store实例,{storeName: store, storeName2: store2}
_e: scope, // effectScope实例
state, // 存储所有store的state状态值
install(app) {
// 注入依赖
app.provide(piniaSymbol, pinia); // 让所有的store都能够拿到pinia实例
app.config.globalProperties.$pinia = pinia;
}
}
return pinia;
}
js
// rootStore
export const piniaSymbol = Symbol();
- 代码6行,创建一个
effectScope
,将该实例挂载到pinia
实例中,命名为_e
。 - 代码7行,创建一个
ref
响应式对象,将对象挂载到pinia
实例中,命名为state
。 - 代码9行,创建一个
Map
对象,store
存储形式是{id1: store1, id2: store2}
的,所以id不允许重复,因此用Map
来做存储会方便许多。 - 代码14行,插件被注册时,需要在
vue
全局属性中挂载$pinia
,同时provide
注入依赖,提供给全局使用,名称跟官方一样用symbol值。
createPinia
方法就基本完成了,是不是非常简单呢,接着进入到核心代码的编写defineStore
。
实现defineStore方法
源码分析
defineStore
有三个入参,分别是idOrOptions、setup、setupOptions
,上一章我们讲到,defineStore
的三种用法。- 可以从源码
1678行
看到,默认是有id
和options
选项,通过类型判断用户入参进行相关的处理。 - 源码
1747
行,返回了useStore
实例给用户调用,再看看useStore
的实现,源码1697
行注入依赖pinia
实例,然后在源码1706
行获取pinia
实例的_s
是否有保存id
这个store
,如果没有就进行初始化,初始化分情况调用了createSetupStore
和createOptionsStore
两种方法。
编写代码
js
// store.js
export function defineStore(idOrOptions, setup) {
let id, options;
const isSetupStore = typeof setup === 'function';
// 如果第一个参数是string,那么它就是id
if (typeof idOrOptions === 'string') {
id = idOrOptions;
options = setup;
} else {
id = idOrOptions.id;
options = idOrOptions;
}
function useStore() {
// 获取当前组件实例,拿到pinia实例
const instance = getCurrentInstance();
const pinia = instance && inject(piniaSymbol);
// 判断是否初始化,如果pinia._s没有这个id,则设置一个
if (!pinia._s.has(id)) {
// 判断第二个参数是否是一个函数,如果是函数那么则是使用compositio形式
if (isSetupStore) {
createSetupStore(id, setup, pinia);
} else {
createOptionsStore(id, options, pinia);
}
}
const store = pinia._s.get(id);
return store;
}
return useStore;
}
- 代码4行,判断第二个参数是否为一个函数,如果是函数,则是
composition
API,如果不是函数,则是options
API。 - 代码6行,判断第一个参数是否为
string
类型,如果是,则它就是该store
的唯一值id
,否则从第二个参数中取id
。 - 代码14行,实现一个
useStore
方法返回store
实例。 - 代码17行,通过
getCurrentInstance
API获取当前的vue
实例,如果有实例,则注入依赖piniaSymbol
获取到pinia
实例。 - 代码19行,从
pinia._s
中通过id
取store
值,如果没取到,进行初始化。 - 代码21行,根据
isSetupStore
判断调用createSetupStore
方法还是createOptionsStore
方法。 - 代码27行,获取到
store
实例,用户调用useStore
时就能获取到对应的store
。
实现createSetupStore和createOptionsStore方法
createSetupStore源码分析
createSetupStore源码有点长,我们截取核心部分分析。
- 源码1403,
partialStore
是pinia
自带的方法和属性,我们在下一章会实现这里面的方法,store
变量是将原属性和用户定义的属性和方法合并。 - 源码1440,将
store
存储到pinia._s
下,方便后面二次读取就无需再进行初始化。 - 源码1443,将用户传入的
setup
也是用effectScope
包一层。 - 源码1445,循环用户传入的
setup
,因为用户传入的setup
是散乱的,他和options
不同,需要判断用户到底写的是state
状态,还是方法,又或者它是一个computed
属性,所以需要循环对这些进行处理 - 循环内容大致逻辑就是,将所有的状态存储到
pinia.state
中,将所有函数重新绑定this
,因为如果用户将方法结构出来使用的话,this
就会错误或丢失,例如:const { increment } = useCounterStore()
,这时调用increment
时,this
并不是指向store
无法取到值。
createSetupStore编写代码
js
// store.js
import { getCurrentInstance, inject, reactive, effectScope, computed, isRef, isReactive } from 'vue';
function isComputed(v) {
return (isRef(v) && v.effect);
}
function createSetupStore(id, setup, pinia, isOptions) {
let scope;
const partialStore = reactive({});
const initState = pinia.state.value[id];
if (!initState) {
pinia.state.value[id] = {};
}
const setupStore = pinia._e.run(() => {
scope = effectScope();
return scope.run(() => setup());
});
function warpAction(name, action) {
return function () {
let res = action.apply(partialStore, arguments);
return res;
}
}
for(let key in setupStore) {
const prop = setupStore[key];
if (typeof prop === 'function') {
setupStore[key] = warpAction(key, prop);
}
if (!isOptions) {
// 如果setup API 需要拿到状态存到全局的state中, computed也是ref,需要另外处理
if ((isRef(prop) && !isComputed(prop)) || isReactive(prop)) {
pinia.state.value[id][key] = prop;
}
}
}
pinia._s.set(id, partialStore);
Object.assign(partialStore, setupStore);
return partialStore;
}
- 代码8行,
reactive
实现一个响应式对象,用于存储pinia
的内置方法。 - 代码9行,先通过
pinia.state
获取state
状态,如果没有,进行初始化。 - 代码11行,给定一个
pinia.state
的默认值为{}
。 - 代码13行,调用
pinia._e
执行run方法,然后再定一个当前store
的effectScope
方法,执行用户传入的setup
函数,effectScope
是可以嵌套使用的,可以假设为pinia._e
是全局的控制的是整个pinia
的响应式,而当前的effectScope
是当前store
局部的,控制的是当前store
的响应式。 - 代码24行,循环所有用户定义的
setup
,判断如果是function
的话,调用warpAction
重新绑定this
的指向。 - 代码32行,需要区分哪些是用户的状态,我们要先了解,
const a = ref(a);
这种是用户定义的状态,所以我们可以调用isRef、isReactive
来判断。但是有一点需要注意,const b = computed()
,此时isRef(b) === true
,就是说computed
也是ref
,我们要另外判断是不是computed
属性。 - 代码3行,判断一个属性是否为
computed
,首先它一定是ref
,其次,判断computed
下是否有effect
这个方法,这里关于vue
源码不多赘述。 - 代码36行,将用户定义的
store
和pinia
内置属性方法合并,存储到pinia._s
中,后面二次调用useStore
时无需再初始化,直接取值就能返回。
createOptionsStore源码分析
- 源码1196行,初始化
pinia.state
的属性,源码1204行,执行用户传入的state
方法(options
API中,所有的state是函数,getter是对象,action是对象)将其存储到全局的state
。 - 源码1209行,获取用户定义的
state
属性,要知道用户定义只是一个普通的值,并不具有响应式,所以需要toRefs
让所有对象转成响应式。 - 源码1217行,处理用户定义的
getter
,用户定义时也是一个普通函数,所以也需要将其处理为computed
,源码1228行.call
绑定的this
一定要指向自己sotre
(通过pinia._s
获取,不用担心获取不到,因为computed
是用户取值时才执行,所以pinia._s
已经存在。 - 代码1233行,调用刚刚的
createSetupStore
方法,可以看到其实options
和composition
都用同一套逻辑,只是当用户使用options
时,我们给他重新组装一个setup
然后交给createSetupStore
函数处理。
createOptionsStore编写代码
js
// store.js
// 创建选项式的store
function createOptionsStore(id, options, pinia) {
const { state, getters, actions } = options;
function setup() {
const localState = pinia.state.value[id] = state ? state() : {};
return Object.assign(toRefs(ref(localState).value), actions, Object.keys(getters).reduce((memo, name) => {
memo[name] = computed(() => {
let store = pinia._s.get(id);
return getters[name].call(store, store);
})
return memo;
}, {}));
}
return createSetupStore(id, setup, pinia, true);
}
- 代码7行,将所有的状态存储到全局的
pinia.state
中。 - 代码8行,处理用户的
state
和getter
,state
转成ref
,而getter
则循环将所有的属性重新用computed
进行绑定即可,跟源码实现差不多。
测试demo
vue
// App.vue
<template>
<div>
<div>{{ store.counter }} / {{ store.dobuleCount }}</div>
<button @click="handleClick">optionsStore</button>
</div>
<div>
<div>{{ store2.counter }}/ {{ store2.dobuleCount }}</div>
<button @click="handleClick2">setupStore</button>
</div>
</template>
<script setup>
import { useCounterStore } from './stores/counter';
import { useCounterStore2 } from './stores/counter2';
const store = useCounterStore();
const store2 = useCounterStore2();
function handleClick() {
store.increment();
}
function handleClick2() {
store2.increment();
}
</script>
options形式定义store
js
// store/counter.js
import { defineStore } from '@/pinia'
export const useCounterStore = defineStore('counterStore', {
state: () => {
return {
counter: 0
}
},
actions: {
increment() {
this.counter++
}
},
getters: {
dobuleCount() {
console.log(this.counter, 111)
return this.counter * 2
}
}
})
compositon形式定义store
js
import { ref, computed } from 'vue'
import { defineStore } from '@/pinia'
export const useCounterStore2 = defineStore('counterStore2', () => {
const counter = ref(0);
function increment() {
this.counter++
}
const dobuleCount = computed(() => {
return counter.value * 2;
})
return { counter, increment, dobuleCount }
})
npm run dev
运行代码看看效果。
结束
我们已经把pinia
的核心功能都实现了一遍,如果有不懂的,欢迎留言或移步至文章开头查看完整源码,跟着逻辑打印一遍就会觉得pinia
非常简单,下一章将讲解pinia
内置的方法的实现。