手撕pinia源码(三):实现pinia内置方法

手撕pinia源码目录

手撕pinia源码(一): pinia的使用

手撕pinia源码(二):实现defineStore

手撕pinia源码(三):实现pinia内置方法

源码地址传送门

$patch实现

对当前状态应用状态补丁。可以同时批量设置状态值,传入一个回调函数,回调函数接收一个store参数可访问当前仓库的所有属性方法。 $patch有两种使用方式

js 复制代码
import { useCounterStore } from './stores/counter';
const store = useCounterStore();
store.$patch({counter: 1000})
// or
store.$patch((data) = {
   data.counter = 1000;
})

代码编写

js 复制代码
// store.js
function isObject(val) {
    return typeof val === 'object' && val !== null;
}

function mergeReactiveObject(target, state) {
    for (let key in state) {
        let oldValue = target[key];
        let newValue = state[key];
        if (isObject(oldValue) && isObject(newValue)) {
            target[key] = mergeReactiveObject(oldValue, newValue);
        } else {
            target[key] = newValue;
        }
    }
    return target;
}
function createSetupStore(id, setup, pinia, isOptions) {
    function $patch(partialStateOrMutatior) {
        // 判断入参类型
        if (typeof partialStateOrMutatior === 'object') {
            mergeReactiveObject(pinia.state.value[id], partialStateOrMutatior);
        } else {
            partialStateOrMutatior(pinia.state.value[id]);
        }
    }

    const partialStore = {
        $patch
    }
    const store = reactive(partialStore);
    ...代码省略
}

修改createSetupStore方法。

  • 代码26行,实现pinia内置方法
  • 代码19行,实现$patch方法
    • 代码21行,判断入参为object,则是第一种用法,那么需要将两个对象合并即可,
      • mergeReactiveObject就是简单的递归对象合并
    • 代码23行,如果不是对象,则是function,此时只需要执行用户传入的函数,并且获取到当前的store返回给用户,让用户操作即可,pinia.state.value[id]ref响应式对象,所以用户可以直接修改就能触发响应。

$reset

通过构建新的状态对象,将存储重置为初始状态。此API只能用于通过options方式构建的store,后面会讲解为何只有options方式构建的store才支持此API。

代码编写

js 复制代码
// store.js
function createOptionsStore(id, options, pinia) {
    ...代码省略
    
    const store = createSetupStore(id, setup, pinia, true); 
    store.$reset = function () {
        const newState = state ? state() : {};
        store.$patch((state) => { 
            Object.assign(state, newState);
        })
    }
    return store;
}
  • 代码6行,给createOptionsStore构建的store添加一个$reset方法
    • 代码7行,重新调用用户定义的state函数,即可获取初始值,所以$reset方法只能用在options定义的store中,因为如果是setup形式的话,我们无法获取到起初始值是什么。 -代码8行,直接调用我们刚刚编写的$patch函数,用初始状态覆盖掉当前状态即可。

$subscribe

设置一个回调,以便在状态更改时调用。它还返回一个函数来删除回调。

代码编写

js 复制代码
// store.js
function createSetupStore(id, setup, pinia, isOptions) {
    function $subscribe(callBack) {
        watch(pinia.state.value[id], (state) => {
            callBack({ storeId: id }, state)
        });
    }
    const partialStore = {
        $patch,
        $subscribe
    }
    ...代码省略
}
  • 代码10行,添加一个$subscribe内置方法
    • 代码3行,实现subscribe方法,
    • 代码4行,代码非常简单,调用vuewatchAPI,监听当前的store属性的变化,当属性变化时,调用用户传递的callBack函数,组装好用户需要的参数返回即可。

$onAction

设置一个回调函数,每次调用操作时执行此回调函数,并且回调函数接收一个参数,参数包含所有操作信息 ,$onAction的使用示例

js 复制代码
import { useCounterStore } from './stores/counter';
const store = useCounterStore();
store.$onAction(function ({after, onError}) {
    after((result) => { // 修改后的结果 })
    after((result) => { // 修改后的结果 })
    after((result) => { // 修改后的结果 })
    onError((error) => { // 执行错误 })
})

观察上面代码,after是可以被多次调用的,我们可以发现,这种形式就是利用发布订阅模式,缓存所有的订阅函数,当调用时执行这些订阅函数。

代码编写

js 复制代码
// 新建一个subscribe.js
export function addSubscription(subscriptions, callback) {
    subscriptions.push(callback);
    const removeSubscription = () => {
        const idx = subscriptions.indexOf(callback);
        if (idx > -1) {
            subscriptions.splice(idx, 1);
        }
    }
    return removeSubscription;
}

export function triggerSubscriptions(subscriptions, ...args) {
    subscriptions.slice().forEach(cb => cb(...args));
}
js 复制代码
// store.js
import { addSubscription, triggerSubscriptions } from './subscribe';
let actionSubscriptions = [];

const partialStore = {
    $patch,
    $subscribe,
    $onAction: addSubscription.bind(null, actionSubscriptions)
}
...代码省略
function warpAction(name, action) {
        return function (callBack) {
            const afterCallbackList = [];
            const onErrorCallbackList = [];
            function after(callBack) {
                afterCallbackList(callBack);
            }
            function onError(callBack) {
                onErrorCallbackList(callBack);
            }
            triggerSubscriptions(actionSubscriptions, {after, onError});
            try {
                let res = action.apply(store, arguments);
                triggerSubscriptions(afterCallbackList, res)
            } catch(error) {
                triggerSubscriptions(onErrorCallbackList, error);
            }

            if (res instanceof Promise) {
                return res.then((value) => {
                    triggerSubscriptions(afterCallbackList, value)
                }).catch(error => {
                    triggerSubscriptions(onErrorCallbackList, error);
                })
            }
            return res;
        }
    }
    ... 代码省略
  • 代码subscribe.js是一个经典的发布订阅模式,如果不了解发布订阅模式可以看我另一篇文章有详细解读。传送门
  • 代码8行,定义一个$onAction的内置方法,当调用这个内置方法的时候,会执行订阅函数收集起来。
  • 代码11行,修改warpAction函数,在调用方法时执行此函数
    • 代码13行,分别定义afterCallbackListonErrorCallbackList,因为afteronError是可以被多次调用的,所以也是一个发布订阅模式
    • 代码15行,提供after方法,当用户调用after时,将参数存入afterCallbackList,代码18行同理。
    • 用户调用action时,触发triggerSubscriptions发布所有的onAction订阅者,入参包含afteronError提供给用户使用。
    • 代码23行,执行完action后,触发triggerSubscriptions发布给所有的after订阅者,入参为最新的状态值。
    • 代码25行,同理,当执行action报错,触发triggerSubscriptions发布给所有的onError订阅者,入参为报错信息。
    • 代码29行,因为action是允许写异步方法的,所以有可能action的执行结果返回的是一个promise,所以判断其是否是Promise,如果是则调用起then方法等待执行完毕后再通知after订阅者,同上。

结束

总结手撕pinia源码系列技术文章,我如果有不懂的,欢迎留言或移步至文章开头查看完整源码,跟着逻辑打印一遍就会觉得pinia非常简单们详细剖析了pinia源码,从源码的角度实现了一个简单版的pinia,当然还有很多特殊场景没有处理,但是能够理解pinia核心我认为就足够了,主要学习的是其设计思想,最后如果有不懂的,欢迎留言或移步至文章开头查看完整源码,跟着逻辑打印一遍就会觉得pinia非常简单。

相关推荐
f8979070706 分钟前
layui动态表格出现 横竖间隔线
前端·javascript·layui
鱼跃鹰飞12 分钟前
Leecode热题100-295.数据流中的中位数
java·服务器·开发语言·前端·算法·leetcode·面试
杨荧24 分钟前
【JAVA开源】基于Vue和SpringBoot的水果购物网站
java·开发语言·vue.js·spring boot·spring cloud·开源
二十雨辰1 小时前
[uni-app]小兔鲜-04推荐+分类+详情
前端·javascript·uni-app
霸王蟹1 小时前
Vue3 项目中为啥不需要根标签了?
前端·javascript·vue.js·笔记·学习
小白求学11 小时前
CSS计数器
前端·css
Anita_Sun2 小时前
🌈 Git 全攻略 - Git 的初始设置 ✨
前端
lucifer3112 小时前
深入解析 React 组件封装 —— 从业务需求到性能优化
前端·react.js
等什么君!2 小时前
复习HTML(进阶)
前端·html
儒雅的烤地瓜2 小时前
JS | 如何解决ajax无法后退的问题?
前端·javascript·ajax·pushstate·popstate事件·replacestate