专栏导航 分析pinia源码之前必须知道的API
Pinia源码分析【1】- 源码分析环境搭建
Pinia源码分析【2】- createPinia
pinia源码分析【3】- defineStore
pinia源码分析【4】- Pinia Methods
前言 本系列文章参考源码pinia V2.0.14
源码分析仓库:https://github.com/vkcyan/goto-pinia
上一章我们对store
的核心流程完成了分析,从而了解了一个store
从定义到被使用的实现逻辑,但是store
相关的方法,我们还未进行分析,本章我们就重点分析分析store
自带的Methods
$onAction 使用示例 订阅当前store
所有action
操作,每当action
被执行的时候,便会触发该方法
1 2 3 4 5 6 7 8 9 10 onMounted (() => { useCounter1.$onAction((option ) => { let { after, onError, args, name, store } = option; }); setInterval (() => { useCounter1.counter ++; }, 1000 ); });
源码分析 订阅 在$Action
声明的地方,我们可以看到一段这样的函数
第一个参数传null
,则不改变this指向,并且在后续的调用依旧是该this。
1 2 3 const partialStore = { $onAction : addSubscription.bind (null , actionSubscriptions), }
也就是说,当我们使用store.$Action
的时候实际上触发的是addSubscription
函数,并将我们$Action
中的回调函数传入createSetupStore
中的actionSubscriptions
中,也就是订阅了我们的callback
运行store.$Action
后得到了addSubscription
方法的返回值removeSubscription
方法,让我们可以执行其返回值,达到取消订阅的目的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 export function addSubscription<T extends _Method>( subscriptions : T[], callback : T, detached?: boolean, onCleanup : () => void = noop ) { subscriptions.push (callback) const removeSubscription = ( ) => { const idx = subscriptions.indexOf (callback) if (idx > -1 ) { subscriptions.splice (idx, 1 ) onCleanup () } } if (!detached && getCurrentInstance ()) { onUnmounted (removeSubscription) } return removeSubscription }
触发订阅 在useStore
中对action
进行处理的逻辑中,存在这样的一段代码,这段代码中的hot在正常使用的业务场景下都是undefined,所以会走后面的逻辑。
1 const actionValue = wrapAction (key, prop)
所有的action
在初始化阶段都会被wrapAction
方法拦截,也就代表我们执行action
的时候,实际上执行的是wrapAction
函数,那就让我们就看看,在wrapAction
中究竟发生了什么
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 function wrapAction (name: string, action: _Method ) { return function (this : any ) { setActivePinia (pinia); const args = Array .from (arguments ); const afterCallbackList : Array <(resolvedReturn: any ) => any> = []; const onErrorCallbackList : Array <(error: unknown ) => unknown> = []; function after (callback: _ArrayType<typeof afterCallbackList> ) { afterCallbackList.push (callback); } function onError (callback: _ArrayType<typeof onErrorCallbackList> ) { onErrorCallbackList.push (callback); } triggerSubscriptions (actionSubscriptions, { args, name, store, after, onError, }); let ret : any; try { ret = action.apply (this && this .$id === $id ? this : store, args); } catch (error) { triggerSubscriptions (onErrorCallbackList, error); throw error; } if (ret instanceof Promise ) { return ret .then ((value ) => { triggerSubscriptions (afterCallbackList, value); return value; }) .catch ((error ) => { triggerSubscriptions (onErrorCallbackList, error); return Promise .reject (error); }); } triggerSubscriptions (afterCallbackList, ret); return ret; }; }
之前在$Action
中的回调函数在此处发挥了作用,每当一个action
触发的都会遍历之前订阅的所有$Action
的回调函数,其内部执行action
方法,action
执行正常在触发after
的callback
,执行异常则触发onError
的callback
。
小结
本质上来说$Action就是一个订阅发布模式。
$Action 订阅者
store.action 发布者
actionSubscriptions - 事件注册中心
triggerSubscriptions - 调度中心
通过订阅者($Action) 把对发布者(action) 的订阅注册到事件注册中心(actionSubscriptions) 中,当发布者(action) 触发时,通知调度中心(triggerSubscriptions) ,调度中心(triggerSubscriptions) 触发事件注册中心中的所有订阅。
$subscribe 使用示例 订阅当前store
中的state
的变化,state
发生任意更改都会触发其回调函数,他还会返回一个用来删除的回调函数
1 2 3 4 5 6 7 8 9 10 11 12 13 let abc = useCounter1.$subscribe( (option, state ) => { let { events, storeId, type } = option; console .log (events, storeId, type, state); }, { detached : false } );
源码分析 当我们使用$subscribe
并传入callback
的时候,首先会将当前的callback
加入注册中心中
1 2 3 4 5 6 const removeSubscription = addSubscription ( subscriptions, callback, options.detached , () => stopWatcher () );
前三个参数经过对$Action
的分析后已经比较熟悉,这里我们重点说明一下第四个参数
stopWatcher
是当前store
中的effectScope
,我们将对当前state
的watch
放入scope
中,以便于销毁store
的时候统一处理。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 const stopWatcher = scope.run (() => watch ( () => pinia.state .value [$id] as UnwrapRef <S>, (state ) => { if (options.flush === "sync" ? isSyncListening : isListening) { callback ( { storeId : $id, type : MutationType .direct , events : debuggerEvents as DebuggerEvent , }, state ); } }, assign ({}, $subscribeOptions, options) ) )
小结
$subscribe
主要依赖vue3
的watch
进行实现,在subscriptions
中注册callback
,但是注册的callback
不通过triggerSubscriptions
进行触发,仅仅作为保存,watch
的触发函数中通过闭包触发$subscribe
中的callback
,达到store
中任意值发生变化的时候都执行callback
的目的
在addSubscription
的返回值removeSubscription
中,不仅会在subscriptions
(注册中心)删除订阅,同时也会执行() => stopWatcher()
,停止watch
监听。达到完全停止监听的目的。
$patch 使用示例 直接更新当前state
,可以通过传入对象 与callback 两种方式进行state
更新,允许传递嵌套值
1 2 3 4 5 6 useCounter1.$patch({ counter : 2 }); useCounter1.$patch((state ) => { state.counter = 2 ; });
源码分析 $patch
的主体逻辑不算很复杂,针对不同的参数类型进行分别处理,其中partialStateOrMutator
是传入的方法,我们将当前store
传入其中,通过其callback
直接完成state
的修改,而传入类型为object
的时候,则通过mergeReactiveObjects
进行处理。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 function $patch (stateMutation: (state: UnwrapRef<S>) => void ): void ; function $patch (partialState: _DeepPartial<UnwrapRef<S>> ): void ; function $patch (partialStateOrMutator: | _DeepPartial<UnwrapRef<S>> | ((state: UnwrapRef<S>) => void ) ): void { let subscriptionMutation : SubscriptionCallbackMutation <S>; isListening = isSyncListening = false ; if (__DEV__) { debuggerEvents = []; } if (typeof partialStateOrMutator === "function" ) { partialStateOrMutator (pinia.state .value [$id] as UnwrapRef <S>); subscriptionMutation = { type : MutationType .patchFunction , storeId : $id, events : debuggerEvents as DebuggerEvent [], }; } else { mergeReactiveObjects (pinia.state .value [$id], partialStateOrMutator); subscriptionMutation = { type : MutationType .patchObject , payload : partialStateOrMutator, storeId : $id, events : debuggerEvents as DebuggerEvent [], }; } const myListenerId = (activeListener = Symbol ()); nextTick ().then (() => { if (activeListener === myListenerId) { isListening = true ; } }); isSyncListening = true ; triggerSubscriptions ( subscriptions, subscriptionMutation, pinia.state .value [$id] as UnwrapRef <S> ); }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 function mergeReactiveObjects<T extends StateTree >( target : T, patchToApply : _DeepPartial<T> ): T { for (const key in patchToApply) { if (!patchToApply.hasOwnProperty (key)) continue ; const subPatch = patchToApply[key]; const targetValue = target[key]; if ( isPlainObject (targetValue) && isPlainObject (subPatch) && target.hasOwnProperty (key) && !isRef (subPatch) && !isReactive (subPatch) ) { target[key] = mergeReactiveObjects (targetValue, subPatch); } else { target[key] = subPatch; } } return target; }
完成对mergeReactiveObjects
的分析后,$patch
的核心逻辑就全部结束了,但是还有一点我们没完成,就是通过$patch
修改的state
,$subscribe
是否可以监听到。
$patch触发$subscribe 在$patch
执行的中,我们会修改当前store
中的state
,$subscribe
中的watch
在flush='sync'
的情况下可以立刻监听到,但是也无法执行callback
,因为$patch
函数最开始的地方将isListening,isSyncListening
置为false
在对值完成修改后,我们将isSyncListening
置为true,并且手动订阅$subscribe
的callback
,达到通过$patch
修改state
也能被$subscribe
监听到的目的。
小结 $patch
的源码相对来说比较简单,但是关于触发$subscribe
的部分代码逻辑比较复杂,尤其是当$subscribe
option
设置中的flush
为sync的时候,修改state
立刻就会触发$subscribe
的watch
,虽然最终呈现出来的结果是一致的,但是内部对不同情况的兼容没有看起来那么简单。
$dispose 调用该方法后将会注销当前store
scope
中存储当前store
中的相关反应,当前state
的watch
,ref
,等等effect
都通过scope.run
创建,就是为了方便统一处理,这里调用scope.stop()
所有的effect
便被全部注销了。
1 2 3 4 5 6 function $dispose ( ) { scope.stop (); subscriptions = []; actionSubscriptions = []; pinia._s .delete ($id); }
$reset 调用该方法可以将当前state
重置为初始化的状态
但是有点需要注意,如果defineStore
通过setup类型
声明,则无法调用该函数
1 2 3 4 5 6 7 const $reset = __DEV__ ? () => { throw new Error ( `🍍: Store "${$id} " is built using the setup syntax and does not implement $reset().` ); } : noop;
如果通过option类型
进行声明,则会重写$reset 方法
1 2 3 4 5 6 7 8 store.$reset = function $reset ( ) { const newState = state ? state () : {}; this .$patch(($state ) => { assign ($state, newState); }); };
总结 至此,我们就完成了对pinia
所有方法的源码解读,而pinia
源码解读系列文章也将告一段落,我们从pinia
的初始化到了解如何实现state,getters 的响应式,最后完成对pinia metnods
的全部解读,也算是完全了解了其核心实现,最后我们将会实现一个mini版的pinia ,仅仅保留核心实现,降低阅读门槛,让大多数人可以轻松了解pinia的核心实现原理~