实现pinia的defineStore,patch,subscribe

234 阅读3分钟

先上流程图,再去看下面代码

pinia.png

defineStore的实现

通过对pinia的defineStote查看可以发现 它是返回了一个函数

defineStore.png

所以我们的实现也是基于这种机制

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>

我来解释下他们的执行调用栈

调用栈.png

这时候 我们再来看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,没错他就是用来缓存函数的执行结果的,为啥这样写的,其实主要是为了以下情况考虑

  1. a页面,调用了我的useProductStore(),并且修改了store的内部值
  2. b页面也调用我的useProductStore()
  3. 这时候,你从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的类型删掉即可