专栏导航
前言
本系列文章参考源码pinia V2.0.14
源码分析仓库:github.com/vkcyan/mini…
上一章我们对store的核心流程完成了分析,从而了解了一个store从定义到被使用的实现逻辑,但是store相关的方法,我们还未进行分析,本章我们就重点分析分析store自带的Methods
$onAction
使用示例
订阅当前store所有action操作,每当action被执行的时候,便会触发该方法
onMounted(() => {
useCounter1.$onAction((option) => {
let { after, onError, args, name, store } = option;
});
setInterval(() => {
useCounter1.counter++;
// useCounter1.increment();
}, 1000);
});
源码分析
订阅
在$Action声明的地方,我们可以看到一段这样的函数
第一个参数传
null,则不改变this指向,并且在后续的调用依旧是该this。
const partialStore = {
$onAction: addSubscription.bind(null, actionSubscriptions), // action事件注册函数
}
也就是说,当我们使用store.$Action的时候实际上触发的是addSubscription函数,并将我们$Action中的回调函数传入createSetupStore中的actionSubscriptions中,也就是订阅了我们的callback
运行store.$Action后得到了addSubscription方法的返回值removeSubscription方法,让我们可以执行其返回值,达到取消订阅的目的。
export function addSubscription<T extends _Method>(
subscriptions: T[], // createSetupStore中的actionSubscriptions
callback: T, // 我们传入的callback
detached?: boolean, // 如果为true,则该$Action在页面销毁之后依旧有效
onCleanup: () => void = noop
) {
// 使用$Action的时候就会触发本函数
subscriptions.push(callback)
const removeSubscription = () => {
const idx = subscriptions.indexOf(callback)
if (idx > -1) {
subscriptions.splice(idx, 1)
onCleanup()
}
}
if (!detached && getCurrentInstance()) {
// 如果detached参数不存在,则在当前页面卸载的时候,去除该订阅事件
onUnmounted(removeSubscription)
}
return removeSubscription
}
触发订阅
在useStore中对action进行处理的逻辑中,存在这样的一段代码,这段代码中的hot在正常使用的业务场景下都是undefined,所以会走后面的逻辑。
const actionValue = wrapAction(key, prop) // hot为undefined的情况下
所有的action在初始化阶段都会被wrapAction方法拦截,也就代表我们执行action的时候,实际上执行的是wrapAction函数,那就让我们就看看,在wrapAction中究竟发生了什么
/**
* 包装一个action来处理订阅
*
* @param name - store的名称
* @param action - 需要被包装的action
* @returns a wrapped action to handle subscriptions
*/
function wrapAction(name: string, action: _Method) {
return function (this: any) {
setActivePinia(pinia);
// 获取当前action的参数
const args = Array.from(arguments);
const afterCallbackList: Array<(resolvedReturn: any) => any> = [];
const onErrorCallbackList: Array<(error: unknown) => unknown> = [];
// 声明after方法
function after(callback: _ArrayType<typeof afterCallbackList>) {
// 将after的call放入list中
afterCallbackList.push(callback);
}
// 声明error方法
function onError(callback: _ArrayType<typeof onErrorCallbackList>) {
onErrorCallbackList.push(callback);
}
// @ts-expect-error
// 触发actionSubscriptions中订阅的store.$Action的全部回调函数,并将参数传入
// 此时store.$Action的callback已经执行,但是after onError的回调函数尚未执行
triggerSubscriptions(actionSubscriptions, {
args,
name,
store,
after,
onError,
});
let ret: any; // ret为action的返回值
try {
ret = action.apply(this && this.$id === $id ? this : store, args);
// handle sync errors
} catch (error) {
// 如果action执行出错,则直接执行错误回调,终止函数
triggerSubscriptions(onErrorCallbackList, error);
throw error;
}
// 如果ret是promise,则当前结果未知,会通过上方的try catch,但是会在action结尾增加then catch进行结果捕捉
if (ret instanceof Promise) {
return ret
.then((value) => {
triggerSubscriptions(afterCallbackList, value);
return value;
})
.catch((error) => {
triggerSubscriptions(onErrorCallbackList, error);
return Promise.reject(error);
});
}
// allow the afterCallback to override the return value
// 如果try catch 通过,并且当前action不是Promise,则逻辑进行到此处,触发所有 触发真正的after函数,并将当前action的返回值传入其中,至此完成对action触发的监听。
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发生任意更改都会触发其回调函数,他还会返回一个用来删除的回调函数
let abc = useCounter1.$subscribe(
(option, state) => {
// 通过store.num = xxxx修改,type为direct
// 通过store.$patch({ num: 'xxx' })修改,type为directpatchObject
// 通过store.$patch((state) => num.name='xxx')修改,type为patchFunction
// storeId为当前store的id
// events 当前改动说明
let { events, storeId, type } = option;
console.log(events, storeId, type, state);
},
{ detached: false }
);
源码分析
当我们使用$subscribe并传入callback的时候,首先会将当前的callback加入注册中心中
const removeSubscription = addSubscription(
subscriptions, // 事件注册中心
callback, // $subscribe传入的callback
options.detached, // 页面卸载的时候是否取消监听
() => stopWatcher() // 执行stopWatcher实际上执行的是scope.run返回的watch,而执行watch的返回函数,也就是停止当前watch
);
前三个参数经过对$Action的分析后已经比较熟悉,这里我们重点说明一下第四个参数
stopWatcher是当前store中的effectScope,我们将对当前state的watch放入scope中,以便于销毁store的时候统一处理。
const stopWatcher = scope.run(() =>
watch(
() => pinia.state.value[$id] as UnwrapRef<S>, // 监听state的变化
(state) => {
// 在不使用$patch的情况下,则两个参数都为true,callback一定会执行
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更新,允许传递嵌套值
// 对象
useCounter1.$patch({ counter: 2 });
// function
useCounter1.$patch((state) => {
state.counter = 2;
});
源码分析
$patch的主体逻辑不算很复杂,针对不同的参数类型进行分别处理,其中partialStateOrMutator是传入的方法,我们将当前store传入其中,通过其callback直接完成state的修改,而传入类型为object的时候,则通过mergeReactiveObjects进行处理。
function $patch(stateMutation: (state: UnwrapRef<S>) => void): void; // Fun传参
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;
// reset the debugger events since patches are sync
/* istanbul ignore else */
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;
// 在上方逻辑中,我们将isListening isSyncListening 重置为false,不会触发$subscribe中的callback,所以需要手动进行订阅发布
triggerSubscriptions(
subscriptions,
subscriptionMutation,
pinia.state.value[$id] as UnwrapRef<S>
);
}
// $patch传入参数为Object的处理逻辑
function mergeReactiveObjects<T extends StateTree>(
target: T,
patchToApply: _DeepPartial<T>
): T {
// no need to go through symbols because they cannot be serialized anyway
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)
) {
// 如果被修改的值 修改前修改后都是object类型并且不是Function类型、并且不是ref 不是isReactive,则递归mergeReactiveObjects达到修改嵌套object的目的
target[key] = mergeReactiveObjects(targetValue, subPatch);
} else {
// @ts-expect-error: subPatch is a valid value
// 如果是简单类型 则直接进行state的修改,这里的target为pinia.state.value[$id]
// 按我们的示例来实际分析:pinia.state.value[$id].counter = 2
target[key] = subPatch;
}
}
return target;
}
完成对mergeReactiveObjects的分析后,$patch的核心逻辑就全部结束了,但是还有一点我们没完成,就是通过$patch修改的state,$subscribe是否可以监听到。
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便被全部注销了。
function $dispose() {
scope.stop();
subscriptions = []; // $subscribe注册中心
actionSubscriptions = []; // $Action的注册中心
pinia._s.delete($id); // 删除effectMap结构
}
$reset
调用该方法可以将当前state重置为初始化的状态
但是有点需要注意,如果defineStore通过setup类型声明,则无法调用该函数
const $reset = __DEV__
? () => {
throw new Error(
`🍍: Store "${$id}" is built using the setup syntax and does not implement $reset().`
);
}
: noop; // noop为空函数
如果通过option类型进行声明,则会重写$reset方法
store.$reset = function $reset() {
// state通过闭包机制获得最初state定义的状态
const newState = state ? state() : {};
// 通过$patch完成对state中数据的更新
this.$patch(($state) => {
assign($state, newState);
});
};
总结
至此,我们就完成了对pinia所有方法的源码解读,而pinia源码解读系列文章也将告一段落,我们从pinia的初始化到了解如何实现state,getters的响应式,最后完成对pinia metnods的全部解读,也算是完全了解了其核心实现,最后我们将会实现一个mini版的pinia,仅仅保留核心实现,降低阅读门槛,让大多数人可以轻松了解pinia的核心实现原理~