先上流程图,再去看下面代码
defineStore的实现
通过对pinia的defineStote查看可以发现 它是返回了一个函数
所以我们的实现也是基于这种机制
const defineStore = <T>(callback: Function) => {
let result: Ctx & T
return (): (Ctx & T) => {
if (result) {
return result
} else {
const val = callback()
result = val
addMethods(val)
return result
}
}
}
调用如下
import { ref,computed } from 'vue'
const useProductStore = defineStore<Result>((): Result => {
const productNum = ref(5)
const productTotalPrice = computed(() => productNum.value * 5)
const changeProductNum = (num: number) => {
productNum.value = num
}
return {
productNum,
changeProductNum,
productTotalPrice
}
})
页面引用
<template>
<div>
<div>页面数据{{ store.productNum }}</div>
<button @click="store.changeProductNum(5)">修改数据</button>
</div>
</template>
<script setup lang="ts">
import { useProductStore } from './index'
const store = useProductStore()
</script>
我来解释下他们的执行调用栈
这时候 我们再来看defineStoe
const defineStore = <T1 extends string, T2>(id: T1, callback: Function) => {
let result: Ctx & T2
return (): (Ctx & T2) => {
if (result) {
return result
} else {
const val = callback()
result = val
// 给result添加修改方法和监听state值变化的方法
addMethods(val, id)
return result
}
}
}
他的执行结果返回的是一个函数,而且大家应该发现了result,没错他就是用来缓存函数的执行结果的,为啥这样写的,其实主要是为了以下情况考虑
- a页面,调用了我的useProductStore(),并且修改了store的内部值
- b页面也调用我的useProductStore()
- 这时候,你从a页面跳转到b发现,就会发现数据没同步,b页面的值还是初始化,你在a页面做的改变,完全没有应用b页面,这是因为如果你不缓存执行结果,得到的就是两个对象,类似与为啥vue2的data为啥是一个函数,因为只这样才数据才能具有独立性,否则都是引用的同一个对象。所以要把函数的执行结果保存起来,这样你后续所有的操作,其实都是对这个对象的数据进行操作的,这样就可以保证,数据的一致性 这就是definestore的实现机制
subscribe的实现
利用闭包的机制,保持对函数执行结果的引用,然后利用vue提供的watch函数来实现,我们都知道,watch可以对reactive和ref包装的值,进行监听 所以,我们只要拿到当前函数的执行结果的那个对象就可以实现 通过的defineStore可以发现,我们在执行函数结果的时候,调用了addMethods
const defineStore = <T1 extends string, T2>(callback: Function) => {
let result: Ctx & T2
return (): (Ctx & T2) => {
if (result) {
return result
} else {
const val = callback()
result = val
addMethods(val, id)
return result
}
}
}
const addMethods = (ctx: Ctx) => {
addSubscribe(ctx)
}
通过以上代码发现 我们定义的时候,就已经储存起来了 所以subscribe方法实现如下
const getWatchData = <T extends Object>(data: T) => {
let result: { [key: string]: unknown } = {}
Object.keys(data).forEach((key) => {
const item = data[key as keyof T]
// 主要是为了过滤函数和计算属性,确保只拿到ref和reactive的值进行监听
if (!isReadonly(item) && (isRef(item) || isProxy(item))) {
result[key] = item
}
})
return reactive(result)
}
const addSubscribe = (ctx: Ctx) => {
const wathData = getWatchData(ctx)
ctx.$subscribe = (callback) => {
watch(wathData, (newVal) => {
callback(ctx, newVal)
})
}
}
通过getWatchData获取要监听的对象(只要ref和reactive包装的变量,其他的都过滤掉,因为函数和计算属性监听没有任何意义)这里借助vue提供的isRef、isProxy、isReadonly就可以过滤拿到我们需要的数据,然后我们在用reactive包装我们得到的结果,至此subscribe就实现了 再来说说patch
patch的实现
其实也是利用闭包的机制,保持对函数执行结果的引用,当调用patch方法的时候,去做比较和替换,具体实现如下
const addMethods = (ctx: Ctx) => {
addPatch(ctx)
}
const defineStore = <T1 extends string, T2>(id: T1, callback: Function) => {
let result: Ctx & T2
return (): (Ctx & T2) => {
if (result) {
return result
} else {
const val = callback()
result = val
addMethods(val, id)
return result
}
}
}
const addPatch = (ctx: Ctx) => {
ctx.$patch = (val) => {
if (isObject(val)) {
for (let key in val) {
const currentItem = val[key as keyof Object]
const proxyItem = ctx[key as keyof Object]
if (proxyItem !== undefined) {
if (isRef(proxyItem)) {
proxyItem.value = currentItem
}
if (isReactive(proxyItem) && isObject(currentItem)) {
Object.assign(proxyItem, currentItem)
}
} else {
try {
console.error(`当前修改的key值---->${key}`)
console.error(`有效的key如下--->`, Object.keys(getWatchData(ctx)).join(','))
} catch (error) {
console.error(error)
}
}
}
} else {
console.error(`$patch函数调用参数必须传递对象,请检查传入的值${val}`)
}
}
}
主要就是根据ref和reacive的不同,来进行不同的替换
小结
整体看下来,大家看过就会发现,其实我都没写多少,主要还是基于vue本身提供的函数+闭包来实现,哈哈,原理大概是这样,但是实际的pinia的要比这个复杂的多,看我的这个就是让大家了解下实现原理,真用还是pinia,毕竟那个无论是兼容性还是边界情况都有考虑的,大家要是想验证 直接复制代码就行 把ts的类型删掉即可